79

LibraryLink is an API for extending Mathematica through C or C++. It is very fast because it gives direct access to Mathematica's packed array data structure, without even needing to make a copy of the arrays.

Unfortunately, working with LibraryLink involves writing a lot of tedious boilerplate code.

Mathematica 10 introduced managed library expressions, a way to have C-side data structures automatically destroyed when Mathematica no longer keeps a reference to them. I find this feature useful for almost all non-trivial LibraryLink projects I make, but unfortunately it requires even more boilerplate.

For me, writing all this repetitive code (and looking up how to do it in the documentation) has been the single biggest barrier to starting any LibraryLink project. It just takes too much time. Is there an easier way?

Szabolcs
  • 234,956
  • 30
  • 623
  • 1,263

1 Answers1

69

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:

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; 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 in its Documentation subdirectory.

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 Reals. 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.

RealTensorRef (an alias for TensorRef<double>) has a number of convenience features:

  • 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.

  • Iteration through elements using begin() and end(). Range-based for loops work, e.g. we could have used for (auto &x : vec) sum += x;.

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. The symbol MeanVariance is created in the context

LClassContext[]
(* "LTemplate`Classes`" *)

This context is added to the $ContextPath 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:

?MeanVariance

class MeanVariance:
    Void compute(Constant List<Real, _>)
    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, and take a look at the many examples that come with the package!

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, which proves its feasibility for use in large projects.

There are additional features such as:

  • Multiple related classes in the same template
  • Support for additional data types, such as SparseArray, RawArray, Image/Image3D
  • 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); unknown exceptions are also caught to prevent killing the kernel when possible.
  • Calling Print for debugging (also through a C++ streams interface), 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 easier LinkObject-based passing

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). Warning: when using this method, there's no guarantee about which expression will be destroyed last! To fix this, 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). However, RawArray and Image may be passed without an explicit element-type specification.

Which LibraryLink features are not supported?

  • The numerical type underlying tensors must be explicitly specified. Tensors without explicitly specified types are not supported. In the future tensors of unspecified types may be handled through C++ templates.

  • There's no explicit support for library callback functions yet, but they can be used by accessing the standard LibraryLink API (function pointers in mma::libData).

Feedback

Contributions and ideas for improvements are most welcome! Feel free to email me. If you find a bug, file a bug report.


Source of LTemplate-MeanVariance.cpp:

#define LTEMPLATE_MMA_VERSION  1120

#include "LTemplate.h"
#include "LTemplateHelpers.h"
#include "MeanVariance.h"


#define LTEMPLATE_MESSAGE_SYMBOL  "LTemplate`LTemplate"

#include "LTemplate.inc"


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::TensorRef<mint> res = mma::detail::get_collection(MeanVariance_collection);
    mma::detail::setTensor<mint>(Res, res);
    return LIBRARY_NO_ERROR;
}


extern "C" DLLEXPORT mint WolframLibrary_getVersion()
{
    return 3;
}

extern "C" DLLEXPORT int WolframLibrary_initialize(WolframLibraryData libData)
{
    mma::libData = libData;
    {
        int err;
        err = (*libData->registerLibraryExpressionManager)("MeanVariance", MeanVariance_manager_fun);
        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)
{
    mma::detail::MOutFlushGuard flushguard;
    const mint id = MArgument_getInteger(Args[0]);
    if (MeanVariance_collection.find(id) == MeanVariance_collection.end()) { libData->Message("noinst"); return LIBRARY_FUNCTION_ERROR; }

    try
    {
        mma::TensorRef<double> var1 = mma::detail::getTensor<double>(Args[1]);

        (MeanVariance_collection[id])->compute(var1);
    }
    catch (const mma::LibraryError & libErr)
    {
        libErr.report();
        return libErr.error_code();
    }
    catch (const std::exception & exc)
    {
        mma::detail::handleUnknownException(exc.what(), "MeanVariance::compute()");
        return LIBRARY_FUNCTION_ERROR;
    }
    catch (...)
    {
        mma::detail::handleUnknownException(NULL, "MeanVariance::compute()");
        return LIBRARY_FUNCTION_ERROR;
    }

    return LIBRARY_NO_ERROR;
}


extern "C" DLLEXPORT int MeanVariance_mean(WolframLibraryData libData, mint Argc, MArgument * Args, MArgument Res)
{
    mma::detail::MOutFlushGuard flushguard;
    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();
    }
    catch (const std::exception & exc)
    {
        mma::detail::handleUnknownException(exc.what(), "MeanVariance::mean()");
        return LIBRARY_FUNCTION_ERROR;
    }
    catch (...)
    {
        mma::detail::handleUnknownException(NULL, "MeanVariance::mean()");
        return LIBRARY_FUNCTION_ERROR;
    }

    return LIBRARY_NO_ERROR;
}


extern "C" DLLEXPORT int MeanVariance_variance(WolframLibraryData libData, mint Argc, MArgument * Args, MArgument Res)
{
    mma::detail::MOutFlushGuard flushguard;
    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();
    }
    catch (const std::exception & exc)
    {
        mma::detail::handleUnknownException(exc.what(), "MeanVariance::variance()");
        return LIBRARY_FUNCTION_ERROR;
    }
    catch (...)
    {
        mma::detail::handleUnknownException(NULL, "MeanVariance::variance()");
        return LIBRARY_FUNCTION_ERROR;
    }

    return LIBRARY_NO_ERROR;
}
Szabolcs
  • 234,956
  • 30
  • 623
  • 1,263
  • 9
    This is an amazing work ! – faysou Oct 04 '15 at 14:28
  • Any chance of version 9 compatibility? – wxffles Oct 05 '15 at 02:48
  • @wxffles Managed library expressions, which I use a lot, are only available in version 10. This was the main reason why it had to be v10-only. Unfortunately it's not possible to make it work in v9, it's too dependent no v10-only features. – Szabolcs Oct 05 '15 at 06:19
  • @Szabolcs Would it make sense to use this to write an updated version of pythonika for ipython? – M.R. Nov 03 '15 at 16:22
  • @M.R. No. IMO such interfaces should use MathLink, not LibraryLink. – Szabolcs Nov 03 '15 at 19:55
  • Wow, this is really, really great! Question: In LFun["compute", {{Real, _, "Constant"}}, "Void"],, what does "Constant" mean? – Niki Estner Nov 10 '15 at 14:20
  • @nikie The argument types are specified in the same way as in LibraryFunctionLoad (with minor restrictions and extensions). So it just refers to constant passing, as described in the LibraryLink tutorial: a packed array will be passed to the function without making a copy of it first, and it is assumed that the function will not modify the array. It is a way to avoid unnecessary copying. – Szabolcs Nov 10 '15 at 15:23
  • @nikie Please do consider it as an experimental thing though ... I am hoping to learn enough from making this first version of LTemplate that I can make the next version much more useful. To do that I need to find out how LTemplate holds up in real-world usage. For IGraph/M, it worked pretty well so far, though there are some things for which I haven't yet found ideal solutions (pass a managed library expression through MathLink-based passing is still hanging in the air and done through a hack in IGraph/M). – Szabolcs Nov 10 '15 at 15:29
  • @nikie The next project, if I ever have time for it, would be accessing ITK though LTemplate. For that I'd need to add MImage support. The challenge is to make it really convenient to use with heavily templated C++ code such as ITK. I would like ITK filters to operate on any type of MImage. I guess you're more interested in this kind of application. Unfortunately I have next to no experience with image processing, but the other day I had a need for ITK. (eventually I did find Image`ITK`). – Szabolcs Nov 10 '15 at 15:32
  • Ah, I see now. I've looked in the reference page for LibraryFunctionLoad, but memory management options weren't mentioned there. I've used LTemplate to access "libSVM" for machine learning (more options and faster than Classify), and it worked perfectly! Thank you very much for this. – Niki Estner Nov 10 '15 at 15:46
  • @nikie I'm glad to hear it! Also, it's good that you let me know that you are using it, now I will be more careful about breaking things ;-) The "Constant" thing is documented in the tutorial, see my link above. I tried to link to the correct location within the page but it didn't work, so you'll have to scroll down and find it .. – Szabolcs Nov 10 '15 at 15:48
  • time to self-Accept perhaps? :^) – Mr.Wizard May 26 '16 at 14:57
  • Hi, @Szabolcs, thank you so much for your package. I am planning to use it to speed up some parts of my code. I encountered a problem. Following LTemplateTutorial.nb, when I run CompileTemplate[template], I got "CreateLibrary::cmperr: Compile error: collect2.exe: error: ld returned 1 exit status". Though .cpp file is generated, but LoadTemplate[template] complains that "LibraryFunction::notfound: Symbol DemoLib not found." What is wrong? – matheorem May 14 '17 at 01:17
  • @Szabolcs I am using mingw. This is my setting $CCompiler = { "Compiler" -> GenericCCompiler, "CompilerInstallation" -> "C:\\mingw-w64\\x86_64-7.1.0-win32-seh-rt_v5-rev0\\mingw64\\bin\\", "CompilerName" -> "x86_64-w64-mingw32-g++.exe"}; – matheorem May 14 '17 at 01:18
  • @matheorem This problem is unrelated to the package. It is about setting up LibraryLink in general. I do not use MinGW with Mathematica. I am sure it can be set up, but it is not officially supported, and there are concerns about compatibility with the MathLink libraries. If there is a problem, it may only manifest itself occasionally. Unless you have a really good reason not to, I recommend you just use the Microsoft compiler. This works, no GUI tools needed. – Szabolcs May 14 '17 at 06:27
  • @matheorem To see the exact errors, you can use the same options as in CreateLibrary, i.e. "ShellCommandFunction" -> Print, "ShellOutputFunction" -> Print – Szabolcs May 14 '17 at 06:30
  • @matheorem For potential issues, see the link in my first comment here: https://mathematica.stackexchange.com/a/55037/12 As for what is going on wrong on your machine: it seems to be a linking error (but you need to check the full error message based on my previous comment). My naive guess about such errors is that it is either because some libraries were not found (i.e. you need to set the path to them somehow), or because they are not compatible (the link I mentioned earlier). As I said, personally I chose not to bother with MinGW, but I would like to see how to get it working ... – Szabolcs May 14 '17 at 07:46
  • Hi, @Szabolcs. Thank you so much for patient reply. My mingw-w64 is working fine in other cases, and my own librarylink function which uses Eigen. I turn on ShellOutputFunction and it points the source of error, here is full output https://pastebin.com/c9bGXmnT . In short, it complains about undefined reference toMLPutFunction'` and other ML* functions. I don't know how to fix it. – matheorem May 14 '17 at 11:21
  • @matheorem I might not be able to help at all, but let's try. Please give me the ShellCommandFunction too and let us continue in chat. – Szabolcs May 14 '17 at 11:55
  • 1
    @Szabolcs This is the most useful external package that I have ever used. Thank you so much! – Henrik Schumacher Jul 29 '17 at 08:27
  • Just from browsing through the documentation I can only concur with Henrik; this is a very finely crafted work; production-grade software, no less. Thank you! – LLlAMnYP May 18 '18 at 10:15
  • @LLlAMnYP I just released a bugfix release. If you use it, please upgrade. There was a problem with iterating through elements of "very" sparse arrays. If you find any problems, let me know. – Szabolcs May 21 '18 at 11:20
  • Thanks for the update. I've taken heed of your advice to first use the tutorials in wolfram groups, so I'll keep this in mind before starting to really use your package. – LLlAMnYP May 21 '18 at 13:29
  • Perhaps you can suggest what's wrong here? I'm stumbling on the first example of your tutorial, CompileTemplate[template, "ShellCommandFunction" -> Print, "ShellOutputFunction" -> Print ] gives me this output. – LLlAMnYP May 22 '18 at 11:43
  • @LLlAMnYP AFAIK Mathematica does not support MinGW out of the box. I assume you configured it yourself (i.e. you set up a compiler for Mathematica yourself instead of letting it detect it). The configuration seems to be incomplete. It should link against the MathLink libraries (possible something like -lMLi4, but I'm not sure). I also seem to remember that it was necessary to convert these libraries before they would work with gcc. If you don't have a good reason to use gcc, you'll save a lot of trouble by sticking to MSVC. – Szabolcs May 23 '18 at 06:43
  • @Szabolcs MMA doesn't detect MSVC for me. I have to manually specify $CCompiler = {"Compiler" -> GenericCCompiler, "CompilerInstallation" -> "D:\\Larkin\\Dev-C++\\MinGW64", "CompilerName" -> "x86_64-w64-mingw32-gcc.exe", "CompileOptions" -> "-O2"} for compilation to C to work (but not in this case, clearly). Do I actually need the full install of Visual Studio or are the redistributables enough? – LLlAMnYP May 23 '18 at 10:10
  • In fact, on my other machine I have VS2017 installed, but MMA 10.2 doesn't autodetect a compiler, and, in fact, CCompilerDriver`VisualStudioCompiler`VisualStudioCompiler is not among the output of CCompilers[Full] – LLlAMnYP May 23 '18 at 10:13
  • @LLlAMnYP I believe Mma 10.2 should work with MSVC 2015, after doing this. I won't work with 2017, only with 2015 and earlier. I use this when I'm on Windows, i.e. only command line tools, not the full VS. – Szabolcs May 23 '18 at 10:16
  • @LLlAMnYP Otherwise you can try adding -lML64i3 (not i4 for M10.2) to the linking options in the compiler setup. You may also need to convert libraries to the appropriate format ... I am guessing here because I don't have a Windows computer handy and I have not used gcc with Mathematica for a long time because of these difficulties. – Szabolcs May 23 '18 at 10:22
  • I mean "It won't work" not "I won't work". – Szabolcs May 23 '18 at 10:22
  • Ok, I see which direction to move in. Thanks. – LLlAMnYP May 23 '18 at 10:28
  • I like this program. – xpaul Mar 26 '24 at 19:08