Search code examples
c++matlabpointersscopemex

Persist C++ class instance through Matlab mex function calls


Currently trying to figure out how to solve this problem.

Right now I am trying to create mex functions for a C++ library which talks to a microcontroller over serial ports on a Windows 10 PC so that I can call functions in that library in matlab. I am currently working through how to persist an instance of my class through multiple matlab mexFunction calls.

So far the only thing I could come up with is to write a wrapper around the class, declare a global extern unique pointer to my class instance, and include it in my mexFunction() files.

Can anyone tell me if this might work, and if so, how exactly does matlab/C++ handle the mexFunction files and their method calls? The scope of my class instance is what I'm unsure of.

A concrete example might be...

What would happen if I declared an extern unique pointer to an object in a .cpp file and included it in my mexFunction files? Would the pointer stay in scope throughout the matlab script that calls multiple different mexFunctions that manipulate that object?

If I need to rephrase the question or provide more information, please let me know.


Solution

  • Yes, you can do this. If the MEX-files all link to the same shared library (DLL), then they all have access to global variables defined in it. You would need to define your global object in the shared library, not in one of the MEX-files.

    MEX-files stay loaded in memory after first execution, until you call clear functions (or clear all). The global object will be destructed when the shared object is cleared from memory. To prevent undesired clearing of your state, you can lock one of the MEX-files in memory using mexLock. I would recommend having one ‘initialize` MEX-file, that constructs the object and locks itself in memory. With a special parameter you can make it unlock itself and destroy the object.


    Here is an example:

    • libXYZ.dylib / libXYZ.so / XYZ.dll -- a shared library, contains a std::shared_ptr<XYZ>.

    • XYZ_set.mex... -- a MEX-file that initializes the XYZ object, and locks itself in memory. Links to the libXYZ shared library.

    • XYZ_get.mex... -- another MEX-file that links to the libXYZ shared library and accesses the XYZ object created by the other MEX-file.

    XYZ_lib.h:

    #include <memory>
    #include <iostream>
    
    struct XYZ {
       XYZ(double a);
       ~XYZ();
       double get();
    private:
       double a_;
    };
    
    extern std::unique_ptr<XYZ> XYZ_data;
    

    XYZ_lib.cpp:

    #include "XYZ_lib.h"
    
    std::unique_ptr<XYZ> XYZ_data;
    
    XYZ::XYZ(double a) : a_(a) { 
       std::cout << "Constructing XYZ with " << a_ << '\n'; 
    }
    
    XYZ::~XYZ() { 
       std::cout << "Destructing XYZ, value was " << a_ << '\n'; 
    }
    
    double XYZ::get() { 
       return a_; 
    }
    

    XYZ_set.cpp:

    #include "XYZ_lib.h"
    #include <mex.h>
    
    /// \brief An output stream buffer for MEX-files.
    ///
    /// Creating an object of this class replaces the stream buffer in `std::cout` with the newly
    /// created object. This buffer will be used as long as the object exists. When the object
    /// is destroyed (which happens automatically when it goes out of scope), the original
    /// stream buffer is replaced.
    ///
    /// Create an object of this class at the beginning of any MEX-file that uses `std::cout` to
    /// print information to the *MATLAB* terminal.
    class streambuf : public std::streambuf {
       public:
          streambuf() {
             stdoutbuf = std::cout.rdbuf( this );
          }
          ~streambuf() {
             std::cout.rdbuf( stdoutbuf );
          }
       protected:
          virtual std::streamsize xsputn( const char* s, std::streamsize n ) override {
             mexPrintf( "%.*s", n, s );
             return n;
          }
          virtual int overflow( int c = EOF ) override {
             if( c != EOF ) {
                mexPrintf( "%.1s", &c );
             }
             return 1;
          }
       private:
          std::streambuf* stdoutbuf;
    };
    
    void mexFunction( int, mxArray*[], int nrhs, const mxArray* prhs[] ) {
       streambuf buf; // Allows std::cout to work in MEX-files
       // Always do lots of testing for correct input in MEX-files!
       if (nrhs!=1) {
          mexErrMsgTxt("Requires 1 input");
       }
       if (mxIsChar(prhs[0])) {
          // Assume it's "-unlock" or something like that. Unlock MEX-file
          mexUnlock();
          std::cout << "XYZ can now be cleared from memory\n";
       } else {
          // Here we create new data
          if (!mxIsDouble(prhs[0]) || mxIsEmpty(prhs[0])) {
             mexErrMsgTxt("Expected double input");
          }
          double a = *mxGetPr(prhs[0]);
          XYZ_data = std::unique_ptr<XYZ>(new XYZ(a));
          // If the MEX-file is not locked, lock it
          if (!mexIsLocked()) {
             mexLock();
          }
       }
    }
    

    (Sorry for the streambuf class here, it's noise, but I wanted to use it so you can see the constructor and destructor in the shared library being called.)

    XYZ_get.cpp:

    #include "XYZ_lib.h"
    #include <mex.h>
    
    void mexFunction( int, mxArray* plhs[], int, const mxArray* [] ) {
       if (XYZ_data) {
          plhs[0] = mxCreateDoubleScalar(XYZ_data->get());
       } else {
          mexErrMsgTxt("XYZ not initialized!");
       }
    }
    

    Compiling:

    In a shell (I'm using MacOS, hence the dylib extension, adjust as necessary):

    g++ -std=c++11 -Wall -fpic XYZ_lib.cpp -shared -o libXYZ.dylib
    

    In MATLAB:

    mex XYZ_set.cpp libXYZ.dylib
    mex XYZ_get.cpp libXYZ.dylib
    

    Running:

    >> XYZ_get
    Error using XYZ_get
    XYZ not initialized!
    
    >> XYZ_set(4)
    Constructing XYZ with 4
    >> XYZ_set(6)
    Constructing XYZ with 6
    Destructing XYZ, value was 4
    >> XYZ_get
    ans =
         6
    >> clear all
    >> XYZ_set -unlock
    XYZ can now be cleared from memory
    >> clear all
    Destructing XYZ, value was 6
    

    As you can see, XYZ_get accesses the value in an object that was newed by XYZ_set. clear all typically clears everything from memory, but here the locked MEX-file stays. XYZ_set -unlock calls it with a string argument, which causes it to unlock itself. clear all now clears that MEX-file from memory also, and now the XYZ object is destroyed.


    I need to mention here that C++ doesn't have a consistent ABI, and these MEX-files will only load if the shared library was compiled with the same compiler.

    An alternative, and often simpler, is to create only one single MEX-file (statically linked with your C++ code), and a bunch of M-files that call the MEX-file. The M-files provide the nice interface (can do input checking also), and the MEX-file sits in a private/ directory where nobody can mess with it. The MEX-file can still do the locking thing, so it can hold on to objects that are preserved from call to call.