Integrating external programs#
This tutorial demonstrates how to call an external executable from SAMSON, in order to perform calculations based on selected atoms in the document.
Sample code#
The source code for an Extension created in this tutorial can be found at SAMSON-Developer-Tutorials.
Generate the SAMSON Extension#
Use the SAMSON Extension Generator to create a new SAMSON Extension called Barycenter, containing an appĀ called SEBarycenterApp
.
Setting up the external executable#
Implement the following simple demo program and save it as barycenter.cpp into the external
subfolder of the generated extension:
#include <sstream>
#include <fstream>
#include <string>
#include <iostream>
int main(int argc, char* argv[]) {
// we expect the program name plus two arguments: input file path and output file path
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " <input-file> <output-file>\n";
return 1;
}
// retrieve the paths from the command-line arguments
std::string inputFilePath = argv[1];
std::string outputFilePath = argv[2];
unsigned int numberOfAtoms;
// open the input file
std::ifstream inputFile(inputFilePath);
if (!inputFile.is_open()) {
std::cerr << "Error: could not open input file: " << inputFilePath << "\n";
return 1;
}
// open the output file
std::ofstream outputFile(outputFilePath);
if (!outputFile.is_open()) {
std::cerr << "Error: could not open output file: " << outputFilePath << "\n";
return 1;
}
// read the number of atoms
std::string line;
std::getline(inputFile, line);
std::stringstream firstLineParser(line);
firstLineParser >> numberOfAtoms;
if (!numberOfAtoms) {
outputFile << "No atoms" << std::endl;
return 0;
}
// read the coordinates of the atoms and update the barycenter
double x_c = 0.0, y_c = 0.0, z_c = 0.0;
while (std::getline(inputFile, line)) {
std::stringstream lineParser(line);
double x = 0.0, y = 0.0, z = 0.0;
lineParser >> x >> y >> z;
x_c += x;
y_c += y;
z_c += z;
}
x_c /= (double)numberOfAtoms;
y_c /= (double)numberOfAtoms;
z_c /= (double)numberOfAtoms;
outputFile << x_c << "A " << y_c << "A " << z_c << "A" << std::endl;
return 0;
}
This is a simple program for demo purposes, it opens an input text file containing atom coordinates and writes into an output file coordinates of the barycenter of the atoms (or the string "No atoms" if the input file did not contain any atom).
Compile it to produce an executable called barycenter
:
- Open the Developer Command Prompt for Visual Studio (or any command prompt where the Visual C++ compiler cl.exe is available).
- Navigate to the directory containing
barycenter.cpp
. -
Compile using the Visual C++ compiler:
Shipping the external executable#
To ship the external executable with your SAMSON Extension:
- in the Extension's folder, create the
external
subfolder with the following subfolders:win
,mac
,lin
(each to place an executable for the corresponding platform); - place the executable into the corresponding folder, e.g. on Windows into
external/win
.
Modify the CMakeLists.txt
file of the extension by adding the following lines that will copy the executable into the Resource
folder shipped with the Extension:
if (WIN32)
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/external/win/barycenter.exe
DESTINATION ${CMAKE_SAMSON_DEVELOPER_DATA_PATH}/Elements/${OUTPUT_UUID}/Resource/barycenter)
endif (WIN32)
if (UNIX AND NOT APPLE)
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/external/lin/barycenter
DESTINATION ${CMAKE_SAMSON_DEVELOPER_DATA_PATH}/Elements/${OUTPUT_UUID}/Resource/barycenter
FILE_PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ)
endif (UNIX AND NOT APPLE)
if (APPLE)
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/external/mac/barycenter
DESTINATION ${CMAKE_SAMSON_DEVELOPER_DATA_PATH}/Elements/${OUTPUT_UUID}/Resource/barycenter
FILE_PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ)
endif (APPLE)
Note
You can place executables in different subfolders and modify the CMakeLists.txt
file accordingly.
Setting up the interface#
Open the UI file form/SEBarycenterAppGUI.ui in QrCreator and add:
- a
QPushButton
namedpushButtonCompute
- a
QLabel
namedlabelResult
Add the slot declaration to the SEBarycenterGUI.hpp file:
In the SEBarycenterGUI
constructor, connect the push button's clicked()
signal to this slot:
QObject::connect(ui.pushButtonCompute, &QPushButton::clicked,
this, &SEBarycenterAppGUI::onCompute);
Modify the getName()
function of the SEBarycenterGUI.cpp file in order to show a user-friendly name (e.g. "Barycenter") in the App menu.
Executing the external executable#
Since the actual barycenter calculations are going to be performed by an external executable, we could assume that it's OK to implement the main functionality of the app in the interface class, i.e. in the file SEBarycenterGUI.cpp. However, if later we wanted to expose the functionality of our SAMSON Extension to other SAMSON Extensions (thanks to the SAMSON's introspection mechanism, explored in another tutorial), we would only be able to expose the app itself, and not its interface. As a result, even in this case, we still want to implement the core functionality in the app itself, and not in its interface.
Now, add the slot definition to the SEBarycenterGUI.cpp file. This just calls the app, and update the label to show the result:
void SEBarycenterAppGUI::onCompute() {
ui.labelResult->setText(QString("Result: ") +
QString::fromStdString(getApp()->compute()));
}
Note that to separate functionality from interface, the app is going to return a std::string
instead of a QString
.
Add the function declaration inside the SEBarycenterApp
in the SEBarycenterApp.hpp header:
We are going to use QProcess
to execute an external process, SAMSON facade class to find atoms in the document, and fstream
to manipulate files, so we add the following headers at the beginning of the SEBarycenterApp.cpp file:
Now, add the function definition at the end of the the SEBarycenterApp.cpp file:
std::string SEBarycenterApp::compute() {
// find all selected atoms in the active document
SBNodeIndexer atomIndexer;
SAMSON::getActiveDocument()->getNodes(atomIndexer,
SBNode::IsType(SBNode::Atom) && SBNode::IsSelected());
// set the temporary files in the SAMSON's scratch folder
const std::string scratchPath = SAMSON::getScratchPath();
const std::string inputFilePath = scratchPath + "/tmp-barycenter-input.txt";
const std::string outputFilePath = scratchPath + "/tmp-barycenter-output.txt";
// generate the input file for the executable
std::ofstream input(inputFilePath);
input << atomIndexer.size() << std::endl;
SB_FOR(SBNode * node, atomIndexer) {
SBAtom* atom = static_cast<SBAtom*>(node);
SBPosition3 position = atom->getPosition();
input <<
SBQuantity::angstrom(position.v[0]).getValue() << " " <<
SBQuantity::angstrom(position.v[1]).getValue() << " " <<
SBQuantity::angstrom(position.v[2]).getValue() << std::endl;
}
// execute the program and wait until it finishes
// Note: the path to the executable corresponds to where it was copied in CMakeLists.txt
QString program = QString::fromStdString(SB_ELEMENT_PATH +
"/Resource/barycenter/barycenter");
#if SB_WINDOWS
program = program + ".exe";
#endif
QStringList arguments{
QString::fromStdString(inputFilePath),
QString::fromStdString(outputFilePath)
};
QProcess* process = new QProcess();
process->setWorkingDirectory(QString::fromStdString(scratchPath));
process->start(program, arguments);
// wait until the process is finished or until time out at 60 seconds
process->waitForFinished(60000);
delete process;
// read the output generated by the external executable
std::ifstream output(outputFilePath);
std::string result;
std::getline(output, result);
// delete the temporary files
QFile(QString::fromStdString(inputFilePath)).remove();
QFile(QString::fromStdString(outputFilePath)).remove();
return result;
}
After compiling and installing (do not forget to build the INSTALL
target), you may now use the app.
Note
In this tutorial, the external executable is expected to be pretty efficient so that we wait for it to finish before continuing. In the general case, you would connect the finished signal of the process to a Qt slot. Please refer to the documentation of QProcess
for more information.
If you have any questions or feedback, please use the SAMSON Connect Forum.