Minimal effort method for integrating C++ functions into Mathematica

119

92

Update: While at the time of writing the question loading DLLs with .NET/Link seemed easier, now I always use LibraryLink, which I recommend to anyone with a similar problem!


As of Mathematica 8, what is the minimal effort way to integrate an existing C++ function into Mathematica?

I think we have these:

  • MathLink (it was quite long ago I used it last time)
  • communication through pipes/files (Import is slow, ReadList not so much)
  • LibraryLink (??)
  • Mathematica 8's C code generation features (??) (apparently this is not relevant)

The keyword here is minimal effort. Which is the most convenient way, including the learning curve for the particular method?

I'm mainly interested in doing it on Windows. My particular function computes a long list of real numbers. The function input consists only of a few scalars (int & double).

An answer of the type "You'll likely spend the least time if you use technology X" is useful. A concrete example of how to do it using a small function is even better.


Answers

Each and every answer I got is great, and each demonstrates a different technology. It's impossible to choose a definitive one. Here's a small "table of contents" for them:

A presentation about the topic from the Wolfram Technology Conference is here (CDF file).


Experiences

I used two of these methods: the .NETLink method and Library Link. These are my personal experiences:

  • .NETLink This was easy to set up, however, I'd recommend it only for when you already have a compiled DLL from which you need to load functions (e.g. do something like this). If you compile your own DLL: once the DLL is loaded, it is locked and cannot be overwritten. A quick and dirty way to allow overwriting it is realoding NETLink with ReinstallNET[].

    Advantages: Very quick and easy, only Mathematica code is needed if the functions are already compiled into a DLL.

    Disadvantages: Windows-only, does not parallelize from within Mathematica, and the calculation will not be interruptible.

  • Library Link It's much easier to use this than what it looks like at first. Less setup is needed than in the case of MathLink, and compilation is automated from within Mathematica.

    Advantages: Also quite easy, but both Mathematica and C code are needed. It is parallelizable, and easily made interruptible. The library can be unloaded (for recompilation) using LibraryUnload.

Szabolcs

Posted 2011-11-15T17:57:27.373

Reputation: 213 047

Is that a List of scalars, or a finite number of parameters? – rcollyer – 2011-11-15T18:04:48.637

@rcollyer By "scalars" I meant that I have a finite number of parameters, each of which is a number. I wanted to point out that I don't need to pass variable (or fixed...) length vectors/list – Szabolcs – 2011-11-15T18:06:20.393

That's what I thought you meant, I was just trying to be clear. – rcollyer – 2011-11-15T18:11:59.533

CCompilerDriver/tutorial/Compilation#472267182 in the Mathematica documentation is probably a good starting point. – Brett Champion – 2011-11-15T18:22:55.140

4The C code generation features are about converting Compile expressions to C code for speed or use them outside Mathematica, rather than accessing existing C code from the kernel. – Joel Klein – 2011-11-16T15:45:51.537

@jfklein Thanks for the comment and the indirect help through Arnoud's answer! It wasn't clear to me if it's possible to use this to intermingle hand-written code with generated one. Some of the answers I got do use some related features to automate the process. – Szabolcs – 2011-11-17T08:50:54.733

Answers

68

Update: I posted a tutorial series on LibraryLink at Wolfram Community.

Update: LibraryLink example code is also available on this GitHub repository.


Here is my pitch to use LibraryLink, which is a really nice new technology in version 8. I am not going to pretend this is easy by any stretch of the imagination, because it involves a decent amount of knowledge of both Mathematica and C compilers. In this particular case I am using Visual Studio C++ Express plus the Microsoft Windows SDK 7.1. For the record, I had quite a bit of help from Joel Klein with this answer.

LibraryLink is set up to find most compilers on your system, but in my case I had to restart after installing the above tools (although looking back I think restarting my open frontend might have done the trick as well).

There are several examples of how to use LibraryLink in the documentation, but this example I wrote from scratch. All LibraryLink C code is compiled with the CreateLibrary function (which is located in the CCompilerDriver package.

Mathematica side

I'll skip the source for now, and focus on the Mathematica commands. First we loads the package to run the library link utility functions:

Needs["CCompilerDriver`"]

Next, we load the source file and create a library (.dll) from it:

myLibrary = 
  CreateLibrary[{"c:\\users\\arnoudb\\myLibrary.c"}, "myLibrary", "Debug" -> False];

Next, we load the one function defined in this library:

myFunction = LibraryFunctionLoad[myLibrary, "myFunction", {{Real, 2}}, {Real, 2}];

Next we use this function:

myFunction[{{1, 2, 3, 4}, {5, 6, 7, 8}}]

which returns the square of every matrix entry: {{1., 4., 9., 16.}, {25., 36., 49., 64.}}.

Finally, we unload the library (needed if we make changes to the source file and reload everything above):

LibraryUnload[myLibrary];

C side

There is a lot of boiler plate magic, to make things 'work'. These first four line need to always be included:

#include "WolframLibrary.h"
DLLEXPORT mint WolframLibrary_getVersion(){return WolframLibraryVersion;}
DLLEXPORT int WolframLibrary_initialize( WolframLibraryData libData) {return 0;}
DLLEXPORT void WolframLibrary_uninitialize( WolframLibraryData libData) {}

This is the actual function that you write. The function header is always the same, you get the actual function arguments from the MArgument_* api functions:

DLLEXPORT int myFunction(WolframLibraryData libData, mint Argc, MArgument *Args, MArgument Res){

 int err; // error code

 MTensor m1; // input tensor
 MTensor m2; // output tensor

 mint const* dims; // dimensions of the tensor

 mreal *data1; // actual data of the input tensor
 mreal *data2; // data for the output tensor

 mint i; // bean counters
 mint j;

This gets the input tensor and dimensions and sets up the output tensor and actual data:

 m1 = MArgument_getMTensor(Args[0]);
 dims = libData->MTensor_getDimensions(m1);
 err = libData->MTensor_new(MType_Real, 2, dims,&m2);
 data1 = libData->MTensor_getRealData(m1);
 data2 = libData->MTensor_getRealData(m2);

The actual interesting stuff, this squares each element:

 for(i = 0; i < dims[0]; i++) {
  for(j = 0; j < dims[1]; j++) {
   data2[i*dims[1]+j] = data1[i*dims[1]+j]*data1[i*dims[1]+j];
  }
 }

This set the return value (and yes, you don't want to return the 'err' value here):

 MArgument_setMTensor(Res, m2);
 return LIBRARY_NO_ERROR;
}

Arnoud Buzing

Posted 2011-11-15T17:57:27.373

Reputation: 9 213

2For me, the case was made with the function CreateLibrary, +1. – rcollyer – 2011-11-16T03:14:13.813

– faysou – 2011-11-16T21:54:50.090

1It looks like I'll go with this approach after all! But there's a little problem in the code: after macro expansion, MArgument_setMTensor(Res, m2) will look like (*(Res.tensor)) = m2, i.e. err = MArgument_setMTensor(Res, m2); will eventually "evaluate" to err = m2, which is incorrect and will also prevent the code from being compiled as C++. Could you correct this? Also, if I got it right, one must add EXTERN_C in front of all the DLLEXPORTs if the code is C++. – Szabolcs – 2011-11-27T12:16:01.680

Another important point when using C++ with the MinGW compiler: it's necessary to pass "Libraries" -> "-lstdc++" to CreateLibrary[], otherwise there will be linking errors for any non-trivial C++ program. Is this a compiler driver bug by any chance? I would have expected this to happen automatically when using "Language" -> "C++" (since the "Libraries" option will be different for different compilers -- so the Mathematica side of my code is now not completely portable) – Szabolcs – 2011-11-27T13:39:35.723

"The full code is here" link is dead in October 2015 -- redirects to a very sketchy location. – hYPotenuser – 2015-10-08T18:21:30.287

Wow, that's bizarre. I've deleted those links until I figure out a better place for it. – Arnoud Buzing – 2015-10-09T18:47:39.457

@Szabolcs Hi, Szabolcs. I tried your option of C++, but failed. For example, I include <complex>, and mma tells me "fatal error: complex: No such file or directory". Arnoud Buzing's answer is very nice and clear, but it is purely on C, not C++, would you please edit his answer and add an complete simple example on C++? – matheorem – 2016-01-11T16:25:26.627

@matheorem On some platforms compiling C++ fails unless you write the code in a separate source file. Writing it in a string won't work, it'll always compile as C. Other than that, it's exactly the same as using C, you just need to use the cpp extension for your files. – Szabolcs – 2016-01-11T16:36:13.590

@Szabolcs why string fails? This will be so inconvenient. I am using windows mingw. You mean on linux string is OK? – matheorem – 2016-01-11T16:39:09.923

@Szabolcs Oh, I tried, now it becomes "Compile error: C:\Users\qq\Documents\test.cpp:2:29: fatal error:
WolframLibrary.h: No such file or directory >>"
– matheorem – 2016-01-11T16:45:47.570

@matheorem I don't know why it doesn't work. You should ask a separate question on describe what you are doing exactly. MinGW-64 is not officially supported but there are questions here on how to get it working. – Szabolcs – 2016-01-11T16:49:41.403

@Szabolcs I was afraid maybe my question is too trivial... Ok, I will ask one. – matheorem – 2016-01-11T16:55:42.010

1@Szabolcs Hi, Szabolcs, I didn't make a post because I figure out the solution. String is also working for C++. But we have to set gcc to treat .c file as c++, this can be done by compile option -x c++. In this way, we don't need "Libraries" -> "-lstdc++" or even "Language" -> "C++". We createlibrary using this CreateLibrary[libnewsource, "func", "CompileOptions" -> "-x c++"] – matheorem – 2016-01-12T01:46:43.667

48

On Windows, C/C++ functions that have been compiled into DLLs can be accessed reasonably easily using NETLink. Let's say we have the following C++ DLL definition:

#include "stdafx.h"

BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved) {
    return TRUE;
}

extern "C" __declspec(dllexport)
void helloMma(double a, double b, int n, double m[]) {
    for (int i = 0; i < n ; ++i) {
        m[i] = a * i + b;
    }
}

We can import this function into Mathematica as follows:

Needs["NETLink`"]

$dllPath = "C:\\some\\path\\to\\hellomma.dll";

helloMma = DefineDLLFunction[
  "helloMma", $dllPath, "void", {"double", "double", "int", "double[]"}
]

Alas the calling sequence for this function is complicated slightly by the fact that it returns its result by destructively overwriting a statically allocated array of doubles (not an uncommon occurrence). To do this from Mathematica, we must pre-allocate the result vector using NETNew:

In[23]:= NETBlock@Module[{n, result}
         , n = 10
         ; result = NETNew["System.Double[]", n]
         ; helloMma[3, 5, n, result]
         ; NETObjectToExpression[result]
         ]
Out[23]= {5., 8., 11., 14., 17., 20., 23., 26., 29., 32.}

Note that all the usual Bad Things would happen if the pre-allocated buffer were overrun. NETBlock is used to ensure that the storage allocated for the buffer is released when we are done with it.

I will point out a couple of "gotchas". Make sure that the DLL is compiled as 32-bit or 64-bit to match the version of Mathematica that you are running. Also, note that the exported DLL function in the example is declared as extern "C". This prevents the C++ compiler from "mangling" the name and makes it easier to reference in the DefineDLLFunction declaration (in this case the Visual Studio C++ mangled name was ?helloMma@@YAXNNHQEAN@Z).

WReach

Posted 2011-11-15T17:57:27.373

Reputation: 62 787

I'm accepting this as a minimal effort solution that worked for me on Windows, but all the answers were great and very useful! – Szabolcs – 2011-11-25T16:19:34.567

Is it possible to unload the DLL so I can recompile the DLL without quitting the kernel? – Szabolcs – 2011-11-25T16:28:06.443

Unloading the DLL might be tricky. If Mma is using .NET interop to load the DLL, then unloading might not be practical -- one would have to load the DLL into a separate application domain and unload that domain. I'm not sure whether Mma would give us control over application domains. If Mma is not using .NET interop, then calls to the standard Win32 API functions GetModuleHandle and FreeLibrary through NETLink might do the trick. This is all speculation on my part at this point -- I have not tried either approach myself. – WReach – 2011-11-25T17:54:57.457

1For practical purposes, ReinstallNET[] works great for "unloading". Of course it destroys everything else .NET related too, but all I wanted was to recompile my DLL without quitting the Mathematica kernel (and I wasn't using .NET for anything else). I should update the post eventually with my experiences with this method and Library Link. The two significant disdvantages of the .NETLink method that remain are that 1. it's Windows only 2. it's not parallelizable from within Mathematica (think ParallelTable). Correct me if I am wrong. – Szabolcs – 2011-12-21T17:15:40.700

1@Szabolcs, in the above example, the use of NETBlock would allow you to recompile your dll file without UninstallNET[] or ReinstallNET[] – unstable – 2012-05-18T11:37:31.783

@unstable That's good to know, thanks for the info! I started using LibraryLink instead of NETLink soon, as it was multiplatform, and more importantly: it can be made abortable. – Szabolcs – 2012-05-18T12:22:23.890

.NET can also used on Mac and Linux via mono, as shown in this video from Wolfram Integrating C and Mathematica

– xslittlegrass – 2013-03-05T04:59:51.203

39

Presuming that your c++ code is already written, then I don't know how the code generation feature would be helpful. That said, in order of simplicity, I would have Get, ReadList, Import, and both LibraryLink and MathLink.


Get and ReadList are by far the simplest. Provided that your c++ program outputs to stdout (std::cout), then it is simply

val = (<<"!command")

or,

ReadList["!command", Number (*or some other specifier*)]

If you need to control your external program more directly, or pass additional data to it (like run your function multiple times), then this method is more difficult and may require you to open pipes to communicate through (1, 2).


Import would require you to conform to some data format, which while very doable, is not as simple as just putting numbers onto stdout.


I have never used LibraryLink, but a quick perusal of the docs implies that it is a simplification of the MathLink interface. More specifically, it is easier to create a function that can communicate with Mathematica, but the intricacies of sending data back and forth remain. To return a List, though, is not to bad, and may well be worth it. The catch, of course, is that you need to provide a c interface to your function, which differs from the MathLink form, but is still necessary. So, I'm not sure there is an advantage here for one or the other.

rcollyer

Posted 2011-11-15T17:57:27.373

Reputation: 32 561

3To the downvoter, I am interested in what was missing from my answer. Did I give to little detail? Or, was it considered incorrect in some way? Either way, would you please elaborate. I'm not trying to complain here, I'm just curious what I could have done instead. – rcollyer – 2011-11-15T21:31:26.727

It's possibly from all the pedestrian traffic in the C++ tag. Surely not a downvote from a Mathematica user. An upvote from me to get you closer to a silver :) – None – 2011-11-16T14:44:47.817

@yoda, you're probably right. They, most likely, got "mad" when I didn't post much code. Oh well. Any way, thanks for the upvote, although I'm still over a hundred shy of the silver. And, I've got to run over Heike first ... :D – rcollyer – 2011-11-16T14:57:33.097

This sort of downvotes seems to just be a fact of life, a drive-by shooting of sorts. I personally think that it is a moral obligation of the downvoter to explain the reasons (I don't downvote myself), but obviously not everyone thinks the same. In any case, here is another one towards your silver - +1 :) – Leonid Shifrin – 2011-11-16T15:41:42.047

@LeonidShifrin, belisarius and I had a long conversation about downvotes and associated messages. For the most part, he doesn't think the messages influence the people who need it, and the people who don't, aren't likely to be downvoted. In this case, I was honestly curious what they figured was needed, especially considering I do pay attention. But, alas, it was not to be. :P (I downvote rarely. Reserving it for incorrect, or misleading, answers. And, usually, it is outside of [tag:mathematica].) – rcollyer – 2011-11-16T15:56:34.790

28

I suggest to use MathLink, which you can automate using the CCompilerDriver`. This is a safe alternative, since you won't crash the kernel if your code crashes. Once tested, this should not be hard to convert to library link. As an explicit example, consider a function which receives a list of integers and squares it. First, here is a function to create the boilerplate code:

makeMLinkCodeF = 
  StringJoin[
     "#include \"mathlink.h\"", "\n", ##, "\n",
     "
     #if WINDOWS_MATHLINK

     #if __BORLANDC__
     #pragma argsused
     #endif

     int PASCAL WinMain( HINSTANCE hinstCurrent, HINSTANCE 
           \ hinstPrevious, LPSTR lpszCmdLine, int nCmdShow)
     {
     \tchar  buff[512];
     \tchar FAR * buff_start = buff;
     \tchar FAR * argv[32];
     \tchar FAR * FAR * argv_end = argv + 32;

     \thinstPrevious = hinstPrevious; /* suppress warning */

     \tif( !MLInitializeIcon( hinstCurrent, nCmdShow)) return 1;
     \tMLScanString( argv, &argv_end, &lpszCmdLine, &buff_start);
     \treturn MLMain( (int)(argv_end - argv), argv);
     }

     #else

     int main(int argc, char* argv[])
     {
     \treturn MLMain(argc, argv);
     }

     #endif"] &;

It is pretty ugly, but you only need to define it once. Perhaps, a better way would be to rewrite it using Symbolic C, but I had no time to do it. Now, here is the code for our function:

code = 
"
 extern void squareList(int * data, long len);

 void squareList(int * data, long len){
    int *d = data;
    MLPutFunction(stdlink,\"List\",len);
    while(d-data<len){
        MLPutInteger(stdlink,(*d) * (*d));
        d++;
    }
 }
";

And here is the template:

template = 
StringReplace[#,"\n"~~Whitespace:>"\n"]&@
"
void squareList P((int *, long));

:Begin:
:Function:       squareList
:Pattern:        squareList[data_List]
:Arguments:      { data }
:ArgumentTypes:  { IntegerList }
:ReturnType:     Manual
:End:

";

(StringReplace was needed since otherwise the template would not copy-paste correctly from SO - it would contain spaces on the left, which would prevent us from creating an executable. You don't have to do this in the interactive session).

We must now load the compiler driver:

Needs["CCompilerDriver`"];

The following code prepares our stuff:

fullCCode = makeMLinkCodeF[code];
projectDir = "C:\\Temp\\MLProject";
If[! FileExistsQ[projectDir], CreateDirectory[projectDir]]
pname = "squareList";
files = 
 MapThread[
    Export[FileNameJoin[{projectDir, pname <> #2}], #1, "String"] &, 
    {{fullCCode, template}, {".c", ".tm"}}];

Now we attempt to create an executable:

In[19]:= exe=CreateExecutable[files,pname]
Out[19]= C:\Users\Archie\AppData\Roaming\Mathematica\SystemFiles\LibraryResources
  \Windows-x86-64\squareList.exe

We now install it:

In[20]:= Install[exe]
Out[20]= LinkObject["C:\Users\Archie\AppData\Roaming\Mathematica\SystemFiles\
LibraryResources\Windows-x86-64\squareList.exe",10,8]

And finally use it:

In[21]:= squareList[Range[5]]
Out[21]= {1,4,9,16,25}


In[23]:= Uninstall[exe]
Out[23]= C:\Users\Archie\AppData\Roaming\Mathematica\SystemFiles\LibraryResources
\Windows-x86-64\squareList.exe

There were a number of steps here, but all of them are within Mathematica, and most of them are common for all functions and can be factored away. If you end up doing lots of coding, I'd create a Mathematica-based DSL which would generate Symbolic C for your functions and also your templates. If / when time permits, I intend to improve this answer by automating the procedure more, and by replacing the fragile string-based pieces with functions to generate them.

Leonid Shifrin

Posted 2011-11-15T17:57:27.373

Reputation: 108 027

Would be very interesting to see the automation stuff :). – nixeagle – 2011-12-19T22:39:38.137

@nixeagle Yes, I also would like to do this, since the string-based manipulations are fragile. This is not really difficult, just need some free time :). – Leonid Shifrin – 2011-12-20T16:41:39.770

15

In terms of loading C++ functions, I would strongly suggest LibraryLink. It is a great tool except that it requires you to write sometimes intimidating C code.

To make LibraryLink easier to use, I have developed a package called wll-interface, available in this github repository. It is a header-only library written in C++, doing only one thing—automatically generate the code that interact with LibraryLink.


A simple example

Here I give an example of how wll-interface simplifies the process of loading a C++ function; this example can also be found here.

Let's say that we have a C++ function multiply and we want to load it into Mathematica.

double multiply(double x, int y)
{
    return x * y;
}

Instead of calling LibraryLink APIs, all we need to do with the help of wll-interface is 1) include the header file, and 2) use DEFINE_WLL_FUNCTION macro. This is what the source code would look like:

src = "
#include \"wll_interface.h\"  // include the header file

double multiply(double x, int y)
{
    return x * y;
}

DEFINE_WLL_FUNCTION(multiply)  // defines wll_multiply
";

Then we create a shared library from the code. You need to replace <path-to-wll_interface.h> below with the directory that contains the header file wll_interface.h so that the compiler can find it.

Needs["CCompilerDriver`"];
mylib = CreateLibrary[src, "wll_multiply"(* name of the library, can be anything *), 
  "IncludeDirectories" -> {"<path-to-wll_interface.h>"}, 
  Language -> "C++", "CompileOptions" -> "-std=c++17"]

Finally we load the function from the shared library we just created. Note that we are loading wll_multiply—a function that is automatically generated.

multiply = LibraryFunctionLoad[mylib, "wll_multiply", {Real, Integer}, Real];

Now multiply can be used just like a normal function!

multiply[2.33, 5]

Manipulating arrays (MTensor)

The template class wll::tensor<type, rank> encapsulates LibraryLink MTensor, making operations on arrays much easier. In this example, I will show you how to pass a reference to an array through LibraryLink and sort the array.

wll::tensor has member functions begin and end to get the iterators, similar to STL containers. Declaring the type of x to be wll::tensor<double, 1>& means that this list of doubles will be passed by reference and no copy will be performed.

src = "
#include <algorithm>
#include \"wll_interface.h\"

void sort(wll::tensor<double, 1>& x)
{
    std::sort(x.begin(), x.end());
}
DEFINE_WLL_FUNCTION(sort)
";

Now we compile the library as before and load the library function wll_sort

Needs["CCompilerDriver`"];
mylib = CreateLibrary[src, "wll_sort",
  "IncludeDirectories" -> {"<path-to-wll_interface.h>"}, 
  Language -> "C++", "CompileOptions" -> "-std=c++17"]
sort = LibraryFunctionLoad[mylib, "wll_sort", {{Real, 1, "Shared"}}, "Void"];

Note that the type of argument is specified as {Real, 1, "Shared"}, where "Shared" means pass by reference here.

This library function works by directly modifies values of the variable in the Wolfram Kernel:

a = RandomReal[1., 10];
sort[a]   (* sort the array; there is no return value *)
a         (* the array has been sorted! *)

Sparse arrays (MSparseArray)

In this example, we will use wll-interface to help us generate identity matrices as sparse arrays. The template class wll::sparse_array<type, rank> corresponds to MSparseArray in LibraryLink. Its member function operator() can be used to extract the elements (wll::tensor also have this function).

src = "
#include \"wll_interface.h\"

wll::sparse_array<double, 2> sparse_identity(size_t size)
{
    wll::sparse_array<double, 2> identity({size, size});
    for (size_t i = 0; i < size; ++i)
        identity(i, i) = 1.0;
    return identity;
}
DEFINE_WLL_FUNCTION(sparse_identity)
";

Here identity(i, i) points to an element on the main diagonal. Following the convention of C/C++, indices in wll-interface are zero-based.

Compile and load the function:

Needs["CCompilerDriver`"];
mylib = CreateLibrary[src, "wll_sparse_identity",
  "IncludeDirectories" -> {"<path-to-wll_interface.h>"}, 
  Language -> "C++", "CompileOptions" -> "-std=c++17"]
identity = LibraryFunctionLoad[mylib, "wll_sparse_identity", {Integer}, LibraryDataType[SparseArray]];
identity[10]  (* gives a 10x10 idensity matrix *)

njpipeorgan

Posted 2011-11-15T17:57:27.373

Reputation: 1 116

1I think this is great work and will have great benefit for people who cares a lot about performance! Many times, I find it easier to write a C++ function and get better performance than to deal with all kinds of surprises in optimizing Mathematica code. I think it will be also more attractive to people, if you could add some examples about matrices and sparse array. – xslittlegrass – 2020-02-23T22:20:40.277

1@xslittlegrass Thanks! I've added two examples of manipulating arrays. – njpipeorgan – 2020-02-24T21:46:29.813

14

You may want to look at this presentation: Integrating C and Mathematica.

In the past, I have found using .NET/Link to be the easiest. You can call C DLL's very easily on Windows, without the need for templates as in MathLink.

asim

Posted 2011-11-15T17:57:27.373

Reputation: 1 855

14

There is a package called LTemplate that automates writing some of the boilerplate code for LibraryLink:

I consider this less effort than writing standard LibraryLink code. In this sense it is a fitting answer for this question.

However, I do recommend familiarizing yourself with the standard way of using LibraryLink before trying LTemplate.

Disclosure: I am the author of LTemplate.

Szabolcs

Posted 2011-11-15T17:57:27.373

Reputation: 213 047