I wrote a package to automatically generate all the boilerplate needed for LibraryLink and for managed library expressions based on a *template* that describes a class interface:
* [LTemplate](https://bitbucket.org/szhorvat/ltemplate)
###Here's how it works
- Write a *template* (a Mathematica) expression that describes a C++ class interface
- This template is used to generate LibraryLink-compatible C functions that call the class's member functions, to compile them, and to finally load them
- Instances of the class are created as [managed library expressions][1]; all the boilerplate code for this is auto-generated
- The package is designed to be embedded into Mathematica applications. This avoids conflicts between different applications using different LTemplate versions. LTemplate can also be used standalone for experimentation and initial development.
###Examples
**Note:** The package comes with a tutorial and many more examples.
While LTemplate is designed to be embedded into other applications, for this small example I'll demonstrate interactive standalone use.
<< LTemplate`
Let us use the system's temporary directory as our working directory for this small example, so we don't leave a lot of mess behind. Skip this to use the current directory instead.
SetDirectory[$TemporaryDirectory]
To be able to demonstrate managed library expressions later, we turn off input/output history tracking:
$HistoryLength = 0;
Let's write a small C++ class for calculating the mean and variance of an array:
code = "
#include <LTemplate.h>
class MeanVariance {
double m, v;
public:
MeanVariance() { mma::print(\"constructor called\"); }
~MeanVariance() { mma::print(\"destructor called\"); }
void compute(mma::RealTensorRef vec) {
double sum = 0.0, sum2 = 0.0;
for (mint i=0; i < vec.length(); ++i) {
sum += vec[i];
sum2 += vec[i]*vec[i];
}
m = sum / vec.length();
v = sum2 / vec.length() - m*m;
}
double mean() { return m; }
double variance() { return v; }
};
";
Export["MeanVariance.h", code, "String"]
LTemplate functions live in the `mma` C++ namespace. `mma::RealTensorRef` represents a reference to a Mathematica packed array of `Real`s. It is just a wrapper for the `MTensor` type. There may be more than one reference to the same array, so array creation and destruction must still be managed manually. A `RealTensorRef` allows convenient linear indexing into the array using the `[ ]` operator. `RealMatrixRef` and `RealCubeRef` are subclasses of `RealTensorRef`, which additionally allow indexing into 2D or 3D arrays using the `( )` operator.
We have three member functions: one for computing both the mean and variance in one go, and two others for retrieving each result.
The class must go in a header file with the same name, `MeanVariance.h` in this case.
Let's write the corresponding template now:
template =
LClass["MeanVariance",
{
LFun["compute", {{Real, _, "Constant"}}, "Void"],
LFun["mean", {}, Real],
LFun["variance", {}, Real]
}
];
We can optionally check that the template has no errors using `ValidTemplateQ[template]`.
Compiling and loading is now as simple as
In[]:= CompileTemplate[template]
Current directory is: /private/var/folders/31/l_62jfs110lf0dh7k5n_y2th0000gn/T
Unloading library MeanVariance ...
Generating library code ...
Compiling library code ...
Out[]= "/Users/szhorvat/Library/Mathematica/SystemFiles/LibraryResources/MacOSX-x86-64/MeanVariance.dylib"
and then
LoadTemplate[template]
The compilation step created a file called `LTemplate-MeanVariance.cpp` in the same directory, the source code of which I attached at the end, for illustration.
We can now create a managed library expression corresponding to this class:
obj = Make["MeanVariance"]
During evaluation of In[]: constructor called
(* MeanVariance[1] *)
arr = RandomReal[1, 10];
obj@"compute"[arr]
{obj@"mean"[], obj@"variance"[]}
(* {0.482564, 0.104029} *)
We can check the list of live expressions of type `MeanVariance` using
LExpressionList["MeanVariance"]
(* {MeanVariance[1]} *)
As soon as Mathematica has no more references to this expression, it gets automatically destroyed
obj =.
During evaluation of In[]: destructor called
LExpressionList["MeanVariance"]
(* {} *)
The reason why we had to use `$HistoryLength = 0` above is to prevent `Out` keeping a reference to the expression.
One practical way to write a Mathematica functions that exposes this functionality is
meanVariance[arr_] :=
Block[{obj = Make[MeanVariance]},
obj@"compute"[arr];
{obj@"mean"[], obj@"variance"[]}
]
As soon as the `Block` finishes, `obj` gets automatically destroyed:
meanVariance[arr]
During evaluation of In[]: constructor called
During evaluation of In[]: destructor called
(* {0.618649, 0.033828} *)
This is one of those special cases when using `Block` over `Module` may be worth it for performance reasons. (The usual caveats about `Block` apply though.)
Notice that the expression has the form `MeanVariance[1]`. The integer index `1` is the [`ManagedLibraryExpressionID`](http://reference.wolfram.com/mathematica/ref/ManagedLibraryExpressionID.html). The symbol `MeanVariance` is created in the context
LClassContext[]
(* "LTemplate`Classes`" *)
This context is added to the [`$ContextPath`](http://reference.wolfram.com/mathematica/ref/$ContextPath.html) when using LTemplate interactively as a standalone package, but not when it's loaded privately by another application. We can check the usage message of the symbol:
<!-- language: lang-none -->
?MeanVariance
class MeanVariance:
Void compute({Real, _, Constant})
Real mean()
Real variance()
----
The package is too large to present fully in a StackExchange post, so if you are interested, download it and read the tutorial, which has several more examples!
LTemplate is continually under development, and breaking changes are possible (though unlikely). However, since the recommended way to deploy it is to embed it fully in the Mathematica application that uses it, this should not be a problem. I am using LTemplate in the [IGraph/M package][2], which proves its feasibility for use in large projects.
There are additional features such as:
* Multiple related classes in the same template
* Pass another managed library expression to a function, and receive it as object reference on the C++ side (`LExpressionID`)
* Format templates to be human-readable (`FormatTemplate`)
* User-friendly error messages
* Error handling through exceptions (`mma::LibraryError`)
* Calling `Print` for debugging, `massert` macro to replace C's standard `assert` and avoid killing the Mathematica kernel.
* Calling `Message`, setting a symbol to associate standard messages with
* Argument passing and return using MathLink (`LinkObject` passing)
* `mlstream.h` auxiliary header for
The documentation isn't complete, but if you have questions, feel free to comment here or email me.
----
###Questions and limitations
**Can I use plain C instead of C++?** No, LTemplate requires the use of C++. However, the only C++ feature the end-user programmer must use is creating a basic class.
**Why do I have to create a class? I only want a few functions.** I didn't implement free functions due to lack of time and need. There's no reason why this shouldn't be added. However, you can always create a single instance of a class, and keep calling functions on it. The overhead will be minimal according to my measurements.
**Why can't I use underscores in class or function names?** LTemplate currently only supports names that are valid *both* in Mathematica and C++. This excludes underscores and $ signs (even though *some* C++ compilers support `$` in identifiers). This also helps avoid name conflicts with auxiliary functions LTemplate generates (which always have underscores).
**How do I write library initialization and cleanup code?** Currently LTemplate doesn't support injecting code into `WolframLibrary_initialize` and `WolframLibrary_uninitialize`. Initialization code can be called manually from Mathematica. Create a single instance of a special class, put the initialization code in one of its member functions, and call it from Mathematica right after loading the template. The uninitialization code can go in the destructor of the class. All objects are destroyed when the library is unloaded (e.g. when Mathematica quits). Dedicated initialization/uninitialization support is planned for later.
**Can I create a function that takes an MTensor of unspecified type?** No, LTemplate requires specifying the data type (but not the rank) of the MTensor. `{Real, 2}` is a valid type specifier and so is `{Real, _}`. `{_, _}` is not allowed in LTemplate, even though it's valid in standard LibraryLink. The same applies to MSparseArrays (`mma::SparseArrayRef`).
----
Source of `LTemplate-MeanVariance.cpp`:
<!-- language: lang-c++ -->
#include "LTemplate.h"
#include "LTemplateHelpers.h"
#include "MeanVariance.h"
namespace mma {
WolframLibraryData libData;
#define LTEMPLATE_MESSAGE_SYMBOL "LTemplate`LTemplate"
#include "LTemplate.inc"
} // namespace mma
std::map<mint, MeanVariance *> MeanVariance_collection;
DLLEXPORT void MeanVariance_manager_fun(WolframLibraryData libData, mbool mode, mint id)
{
if (mode == 0) { // create
MeanVariance_collection[id] = new MeanVariance();
} else { // destroy
if (MeanVariance_collection.find(id) == MeanVariance_collection.end()) {
libData->Message("noinst");
return;
}
delete MeanVariance_collection[id];
MeanVariance_collection.erase(id);
}
}
extern "C" DLLEXPORT int MeanVariance_get_collection(WolframLibraryData libData, mint Argc, MArgument * Args, MArgument Res)
{
mma::IntTensorRef res = mma::detail::get_collection(MeanVariance_collection);
mma::detail::setTensor<mint>(Res, res);
return LIBRARY_NO_ERROR;
}
extern "C" DLLEXPORT mint WolframLibrary_getVersion()
{
return WolframLibraryVersion;
}
extern "C" DLLEXPORT int WolframLibrary_initialize(WolframLibraryData libData)
{
mma::libData = libData;
{
int err;
err = (*libData->registerLibraryExpressionManager)("MeanVariance", MeanVariance_manager);
if (err != LIBRARY_NO_ERROR) return err;
}
return LIBRARY_NO_ERROR;
}
extern "C" DLLEXPORT void WolframLibrary_uninitialize(WolframLibraryData libData)
{
(*libData->unregisterLibraryExpressionManager)("MeanVariance");
return;
}
extern "C" DLLEXPORT int MeanVariance_compute(WolframLibraryData libData, mint Argc, MArgument * Args, MArgument Res)
{
const mint id = MArgument_getInteger(Args[0]);
if (MeanVariance_collection.find(id) == MeanVariance_collection.end()) { libData->Message("noinst"); return LIBRARY_FUNCTION_ERROR; }
try {
mma::RealTensorRef var1 = mma::detail::getTensor<double>(Args[1]);
(MeanVariance_collection[id])->compute(var1);
}
catch (const mma::LibraryError & libErr)
{
libErr.report();
return libErr.error_code();
}
return LIBRARY_NO_ERROR;
}
extern "C" DLLEXPORT int MeanVariance_mean(WolframLibraryData libData, mint Argc, MArgument * Args, MArgument Res)
{
const mint id = MArgument_getInteger(Args[0]);
if (MeanVariance_collection.find(id) == MeanVariance_collection.end()) { libData->Message("noinst"); return LIBRARY_FUNCTION_ERROR; }
try {
double res = (MeanVariance_collection[id])->mean();
MArgument_setReal(Res, res);
}
catch (const mma::LibraryError & libErr)
{
libErr.report();
return libErr.error_code();
}
return LIBRARY_NO_ERROR;
}
extern "C" DLLEXPORT int MeanVariance_variance(WolframLibraryData libData, mint Argc, MArgument * Args, MArgument Res)
{
const mint id = MArgument_getInteger(Args[0]);
if (MeanVariance_collection.find(id) == MeanVariance_collection.end()) { libData->Message("noinst"); return LIBRARY_FUNCTION_ERROR; }
try {
double res = (MeanVariance_collection[id])->variance();
MArgument_setReal(Res, res);
}
catch (const mma::LibraryError & libErr)
{
libErr.report();
return libErr.error_code();
}
return LIBRARY_NO_ERROR;
}
[1]: http://reference.wolfram.com/language/LibraryLink/tutorial/InteractionWithMathematica.html#353220453
[2]: https://github.com/szhorvat/IGraphM