Managed expressions πŸ”—

One of the features offered by LibraryLink is Managed Library Expressions (MLEs). The idea is to create C/C++ objects that will be automatically deleted when they are no longer referenced in the Wolfram Language code. More information can be found in the official LibraryLink documentation.

This allows for an object-oriented programming paradigm and it is the recommended way of referencing C++ objects from the Wolfram Language. The two most notable alternatives are:

  • recreating C++ objects every time a library function is called

  • maintaining some sort of global cache with referenced objects, where each object is added on first use and manually deleted at some point.

LLU uses methods similar to the second alternative to facilitate using MLEs and decrease the amount of boilerplate code needed from developers. Namely, for each class that you register to be used as MLE, LLU will maintain a map, which associates managed C++ objects with IDs assigned to them by the Wolfram Language.

Register a class as Managed Expression πŸ”—

Imagine you have a class A whose objects you want to manage from the Wolfram Language:

struct A {
   A(int n) : myNumber{n} {}
   int getMyNumber() const { return myNumber; }
private:
   int myNumber;
};

Then you must create the corresponding Store and specialize a callback function for LibraryLink (this is a technicality that just needs to be done):

LLU::ManagedExpressionStore<ClassName> AStore;  //usually <class name>Store is a good name

//specialize manageInstanceCallback, this should just call manageInstance function from your Store
template<>
void LLU::manageInstanceCallback<A>(WolframLibraryData, mbool mode, mint id) {
   AStore.manageInstance(mode, id);
}
template<class T>
void LLU::manageInstanceCallback(WolframLibraryData, mbool, mint) πŸ”—

A template for library callback used by LibraryLink to manage instances of ManagedLibraryExpressions.

Specializations should typically just call manageInstance method from the ManagedExpressionStore corresponding to class T.

Template Parameters
  • T: - class to be managed

Note

This function must be explicitly specialized for any class that is supposed to be managed. Therefore instantiation of the general template will trigger compilation error.

Alternatively, you can use a macro:

DEFINE_MANAGED_STORE_AND_SPECIALIZATION(ClassName) πŸ”—

Use this macro to define an instance of ManagedExpressionStore corresponding to your class and a template specialization of manageInstanceCallback for the managed class.

but the macro has some limitations:

  1. it must be invoked from the global namespace

  2. the definition of ClassName must be visible at the point of invocation

  3. ClassName must be an unqualified name (which combined with 1. means that ClassName must be a class defined in the global namespace)

Lastly, you need to register and unregister your type when library gets loaded or unloaded, respectively.

1
2
3
4
5
6
7
8
9
EXTERN_C DLLEXPORT int WolframLibrary_initialize(WolframLibraryData libData) {
   LLU::LibraryData::setLibraryData(libData);
   AStore.registerType("A");   // the string you pass is the name of a symbol that will be used in the Wolfram Language for managing
   return 0;                   // objects of your class, it is a good convention to just use the class name
}

EXTERN_C DLLEXPORT void WolframLibrary_uninitialize(WolframLibraryData libData) {
   AStore.unregisterType(libData);
}

With MLEs in LibraryLink it is not possible to pass arguments for construction of managed expressions. LLU extends the MLE implementation by letting the developer define a library function that LLU will call from the Wolfram Language when a new instance of a managed expression is created. In other words, define a wrapper for constructor of your class. Typically, it will look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
EXTERN_C DLLEXPORT int OpenManagedA(WolframLibraryData libData, mint Argc, MArgument *Args, MArgument Res) {
   auto err = LLU::ErrorCode::NoError;
   try {
      LLU::MArgumentManager mngr(libData, Argc, Args, Res);
      auto id = mngr.getInteger<mint>(0); // id of the object to be created
      auto arg1 = mngr.getXXXX(1);
      auto arg2 = mngr.getYYYY(2);
      ... // read the rest of parameters for constructor of your managed class
      AStore.createInstance(id, arg1, arg2, ...);
   } catch (const LLU::LibraryLinkError& e) {
      err = e.which();
   }
   return err;
}

It is simpler to register an MLE in the Wolfram Language. You only need to load your constructor wrapper:

`LLU`Constructor[A] = `LLU`PacletFunctionLoad["OpenManagedA", {`LLU`Managed[A], Arg1Type (*, ...*)}, "Void"];

Using Managed Expressions πŸ”—

After the registration is done, using MLEs is very simple. In C++ code, MLEs can be treated as another MArgument type, for example let’s define a wrapper library function over A::getMyNumber():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
LIBRARY_LINK_FUNCTION(GetMyNumber) {
   auto err = LLU::ErrorCode::NoError;
   try {
      // create an instance of MArgumentManger for this function
      LLU::MArgumentManager mngr {Argc, Args, Res};

      // get a reference to the Managed Expression of type A, on which this function was called in the Wolfram Language
      const A& myA = mngr.getManagedExpression(0, AStore);

      // set the value of myA.getMyNumber() as the result of this library function
      mngr.set(myA.getMyNumber());

   } catch (const LLU::LibraryLinkError &e) {
      err = e.which();
   }
   return err;
}

In the Wolfram Language, wrappers over member functions can be conveniently loaded:

`LLU`LoadMemberFunction[A][
   getMyNumber,      (* fresh symbol for the member function *)
   "GetMyNumber",    (* function name in the library *)
   {},               (* argument list *),
   Integer           (* result type *)
];

The only thing left now is to create an MLE instance and call the member function on it:

myA = `LLU`NewManagedExpression[A][17];

myA @ getMyNumber[]
(* = 17 *)

API Reference πŸ”—

template<typename T>
class LLU::ManagedExpressionStore πŸ”—

ManagedExpressionStore will keep track of instances of managed class T and will provide safe access to them.

Template Parameters
  • T: - managed class

Public Types

using iterator = typename std::unordered_map<mint, std::shared_ptr<T>>::iterator πŸ”—

Iterator over ManagedExpressionStore - it iterates over the underlying hash map.

using const_iterator = typename std::unordered_map<mint, std::shared_ptr<T>>::const_iterator πŸ”—

Constant iterator over ManagedExpressionStore - it β€œconst-iterates” over the underlying hash map.

using size_type = typename std::unordered_map<mint, std::shared_ptr<T>>::size_type πŸ”—

Size type of the Store is the same as size_type of the underlying hash map.

Public Functions

void manageInstance(mbool mode, mint id) πŸ”—

Function that will actually be called by LibraryLink when an instance of Managed Expression is created or deleted.

Notice that this function does not actually create a new object of class T. This is because there is no way to pass constructor arguments here.

Parameters
  • mode: - are we deleting existing instance (True) or creating new one (False)

  • id: - id of the instance of interest

template<class DynamicType = T, typename ...Args>
T &createInstance(mint id, Args&&... args) πŸ”—

Create new object of class T that will be managed from Wolfram Language and place it in the map of managed objects.

Template Parameters
  • DynamicType: - actual type of the constructed object, it allows Store to keep objects of subclasses of T

  • Args: - constructor arguments types

Parameters
  • id: - id of the newly created managed object

  • args: - constructor arguments

Return

reference to the newly created object

T &createInstance(mint id, std::shared_ptr<T> ptr) πŸ”—

Create instance in the store from a pointer to the managed class object.

This is useful when you have an existing object to be managed or when objects of class T cannot be constructed directly (e.g. because T is an abstract class).

Parameters
  • id: - id of the newly created managed object

  • ptr: - pointer to an instance of T or a subclass

Return

reference to the object just added to the store

T &createInstance(mint id, std::unique_ptr<T> ptr) πŸ”—

Create instance in the store from a unique pointer to the managed class object.

The store will claim shared ownership of the managed object.

Parameters
  • id: - id of the newly created managed object

  • ptr: - pointer to an instance of T or a subclass

Return

reference to the object just added to the store

int releaseInstance(mint id) πŸ”—

Release an instance managed by this Store.

Parameters
  • id: - id of the instance to be released

Return

0 if the id was correct and the operation succeeded, non-negative integer otherwise

Note

Normally, every instance in the Store has a corresponding WL expression and the instance is released as soon as the corresponding expression goes out of scope (its reference count hits 0). This function can be used to force immediate release of a managed instance.

See

https://reference.wolfram.com/language/LibraryLink/ref/callback/releaseManagedLibraryExpression.html

bool hasInstance(mint id) const πŸ”—

Check if instance with given id is present in the store.

Parameters
  • id: - id to be checked

Return

true iff the instance with given id is in the store

T &getInstance(mint id) πŸ”—

Get managed instance with given id.

Throw if the id is invalid or if there is no corresponding instance.

Parameters
  • id: - id of instance of interest

Return

reference to the managed object

std::shared_ptr<T> getInstancePointer(mint id) πŸ”—

Get a shared pointer to a managed instance with given id.

Throw if the id is invalid.

Parameters
  • id: - id of instance of interest

Return

shared pointer to the managed object

const std::string &getExpressionName() const noexcept πŸ”—

Get symbol name that is used in the WL to represent Managed Expressions stored in this Store.

Return

symbol name

size_type size() const noexcept πŸ”—

Get the number of currently managed expressions.

Return

size of the store

iterator begin() noexcept πŸ”—

Get the iterator to the first element of the Store.

const_iterator begin() const noexcept πŸ”—

Get the const iterator to the first element of the Store.

const_iterator cbegin() const noexcept πŸ”—

Get the const iterator to the first element of the Store.

iterator end() noexcept πŸ”—

Get the iterator past the last element of the Store.

const_iterator end() const noexcept πŸ”—

Get the const iterator past the last element of the Store.

const_iterator cend() const noexcept πŸ”—

Get the const iterator past the last element of the Store.

void registerType(std::string name, WolframLibraryData libData = LibraryData::API()) noexcept πŸ”—

Register class T as managed expression under given name.

Parameters
  • name: - name of the Wolfram Language symbol that will be used to manage class T

  • libData: - optionally specify WolframLibraryData instance

Note

This function should typically be called in WolframLibrary_initialize

void unregisterType(WolframLibraryData libData = LibraryData::API()) const noexcept πŸ”—

Unregister class T as managed expression.

Parameters
  • libData: - optionally specify WolframLibraryData instance

Note

This function should typically be called in WolframLibrary_uninitialize