The Best Of Both Worlds

UnRisk-Q uses the expressive power of the Wolfram Language to model the domain specific objects that we deal with in quantitative finance. When it comes to doing computations with these objects, most of the financial algorithms are implemented in C++ because performance is crucial.

The Wolfram Language provides two interfacing technologies for calling C++ programs, MathLink and LibraryLink. As an example, we’ll demonstrate how to call the following simple C++ function which adds a scalar to a vector from a Wolfram Language program:

void add_scalar_to_vector(double* vec, int length, double scalar) {
 std::for_each(&vec[0], &vec[length], [scalar](double& elem) { elem += scalar; });
}

MathLink

MathLink is both a communication protocol and and a programming library for C++. With the advent of Mathematica 10, MathLink has been rebranded as Wolfram Symbolic Transfer Protocol, but we’ll stick with the name MathLink for now.

The mechanism that connects a C++ program with the Mathematica kernel is shown below:

At runtime the C++ program is connected to the kernel through a bidirectional stream which allows for both reading and writing Wolfram Language expressions. The stream may be implemented by a shared memory or a TCP/IP connection.

The C++ function to be called from the Wolfram language needs to be compiled into a standalone executable C++ program which links to the MathLink library. One has to provide a MathLink template file which describes the the pattern to be defined to call the function from the Wolfram language side and the data type mapping from Wolfram language expressions to native C++ function argument types like scalars or arrays:

:Begin:
:Function:       AddScalarToVectorML
:Pattern:        AddScalarToVectorML[vector: {_Real ...}, scalar_Real]
:Arguments:      { vector, scalar }
:ArgumentTypes:  { Real64List, Real64 }
:ReturnType:     Manual
:End:

void AddScalarToVectorML(double* v, int vLen, double s) {
    add_scalar_to_vector(v, vLen, s);
    MLPutReal64List(stdlink, v, vLen);
}

AddScalarToVectorML is the wrapper function which calls our example function add_scalar_to_vector and writes back the result on the MathLink stream represented by the global variable stdlink.

Once the MathLink template files is compiled to an executable program with the CreateExectuable function with

mathLinkExampleExe = CreateExecutable[pathToTemplateFile, "AddScalarToVector",
 "Language"->"C++"] 

the executable can be launched and connected to the kernel with the Install function:

Install[mathLinkExampleExe]

This makes the AddScalarToVectorML function definition available in the Mathematica kernel session:

AddScalarToVectorML[RandomReal[{0, 10}, 10], 10.0] 

MathLink offers the following advantages:

  • It supports running the Wolfram Language and the MathLink executable on different machines, perhaps running different operating systems.
  • It allows you to connect a 64-bit Mathematica kernel to a 32-bit MathLink executable which is necessary for connecting legacy 32-bit libraries.
  • Because the C++ function is run in a separate process, a crash of the MathLink executable will not affect the Mathematica kernel.
  • It is easy to debug a MathLink executable from within an IDE.

These strengths of MathLink have served the Wolfram Language platform well since MathLink’s inception.

Strength is irrelevant

The advantages of MathLink do not make up for its greatest disadvantage:

Arguments passed to and from a MathLink function cannot share data with the Mathematica kernel. Data has to be copied over the link, resulting in execution time and memory consumption overhead, especially with large amounts of data.

Enter LibraryLink

Wolfram LibraryLink provides a way to connect external C++ code to the Wolfram Language, enabling high-speed and memory-efficient execution. It does this by allowing dynamic libraries to be directly loaded into the Mathematica kernel, so that functions in the libraries become part of the kernel process and can be immediately called from the Wolfram Language:

The C++ function to be called from the Wolfram language needs to be compiled into a dynamic library. The dynamic library must export a wrapper function whose argument list conforms to the calling conventions required by LibraryLink:

EXTERN_C DLLEXPORT int AddScalarToVectorLL(
    WolframLibraryData libData, 
    mint Argc, MArgument* Args, 
    MArgument Res)
{
    MTensor vectorT = MArgument_getMTensor(Args[0]);
    if (libData->MTensor_getType(vectorT) != MType_Real) return LIBRARY_TYPE_ERROR;
    if (libData->MTensor_getRank(vectorT) != 1) return LIBRARY_RANK_ERROR;
    mint vLen = libData->MTensor_getFlattenedLength(vectorT);
    double* v = libData->MTensor_getRealData(vectorT);
    mreal s = MArgument_getReal(Args[1]);

    add_scalar_to_vector(v, vLen, s);

    MArgument_setMTensor(Res, vectorT);
    return LIBRARY_NO_ERROR;
}

Once the library source file is compiled to a dynamic library with the CreateLibrary function with

libraryLinkExampleLib=CreateLibrary[{libraryLinkExampleFile}, "AddScalarToVector"]

the generated dynamic library can be loaded into a kernel session with LibraryFunctionLoad:

AddScalarToVectorLL=LibraryFunctionLoad["AddScalarToVector",
"AddScalarToVectorLL", {{Real, 1}, Real}, {Real, 1}]

Unlike MathLink, the required data type mapping from Wolfram language expressions to C++ language function argument types is specified as {{Real, 1}, Real}, {Real, 1} upon loading the function. The result of LibraryFunctionLoad is a pure function which can be called as:

AddScalarToVectorLL[RandomReal[{0, 10}, 10], 10.0]

LibraryLink’s focus on memory and runtime efficiency comes at a price:

  • A crash in a LibraryLink loaded function takes down the Mathematica kernel with it.
  • The platform and architecture of the dynamic library must match the kernel one’s exactly.
  • Debugging a LibraryLink function is more difficult, because the IDE must be attached to a running Mathematica kernel process.

So, with MathLink and LibraryLink a developer has the freedom to choose between safety and speed.

Freedom is irrelevant

Ideally, you want both. During development you want the robustness and the easy debuggability of MathLink. For deployment, you want your native functions to enjoy the time and memory efficiency provided by LibraryLink.

This requirement is met with some resistance from developers, who have to go through the tedious process of writing not only one, but two different sets of wrapper functions for each low level C++ function that should be callable from the Wolfram Language.

Resistance is futile

It turns out there is a way to have the best of both worlds with little effort.

TO BE CONTINUED…

Go to part 2.