Web Analytics Made Easy - Statcounter
Skip to content

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:

    cl /EHsc barycenter.cpp /Fe:barycenter.exe
    

    Compile external program on Windows

  • Navigate to the directory containing barycenter.cpp.
  • Compile using g++:

    g++ barycenter.cpp -o barycenter
    

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 named pushButtonCompute
  • a QLabel named labelResult

UI

Add the slot declaration to the SEBarycenterGUI.hpp file:

public slots:

    void                        onCompute();

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:

std::string                compute();

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:

#include "SAMSON.hpp"
#include <fstream>
#include <QProcess>

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.