In the first part of this blog episode, we gave an overview of the two interfacing technologies that the Wolfram Language provides for calling C++ programs, MathLink and LibraryLink. MathLink’s main advantage is its robustness, whereas LibraryLink offers high-speed and memory efficient execution. Providing support for both technologies at the same time, which is desirable from a software engineering perspective, requires considerable work from developers.
And now the conclusion
It turns out there is a way to have the best of both worlds with little effort. The idea is to replace the two separate wrapper functions that are needed for MathLink and LibraryLink with a single universal wrapper function, which can be easily adapted to both interfacing technologies. In this blog post we’ll rewrite the add_scalar_to_vector
integration example from the first part using such a universal wrapper function.
First let’s recap the LibraryLink based wrapper function from the first part:
EXTERN_C DLLEXPORT int AddScalarToVectorLL(
WolframLibraryData libData,
mint Argc, MArgument* Args,
MArgument Res)
{
...
}
The wrapper function conforms the MArgument
signature required by LibraryLink. The dynamic library generated from that wrapper function was loaded into the Mathematica kernel session with LibraryFunctionLoad:
AddScalarToVectorLL = LibraryFunctionLoad["AddScalarToVector",
"AddScalarToVectorLL", {{Real, 1}, Real}, {Real, 1}]
Your culture will adapt to service ours
LibraryLink also supports an alternate signature for wrapper functions that is based on the data type MLINK
. This type represents a MathLink connection that lets you send any Wolfram Language expression to your library and get back any Wolfram Language expression as a result. Let us rewrite AddScalarToVectorLL
with the MLINK
signature:
EXTERN_C DLLEXPORT int AddScalarToVectorLL(
WolframLibraryData libData,
MLINK mlp)
{
return AddScalarToVector(mlp);
}
We have moved the body of AddScalarToVectorLL
to a new function AddScalarToVector
which represents our universal wrapper function:
int AddScalarToVector(MLINK link) {
int argCount;
if (!MLTestHead(link, "List", &argCount) || argCount != 2)
return LIBRARY_FUNCTION_ERROR;
double* list; int length;
if (!MLGetReal64List(link, &list, &length)) return LIBRARY_FUNCTION_ERROR;
typedef std::unique_ptr<double, std::function<void(double*)>> Real64ListHolder;
Real64ListHolder list_holder(list, [=](double* ptr) {
MLReleaseReal64List(link, ptr, length);
});
double scalar;
if (!MLGetReal64(link, &scalar)) return LIBRARY_FUNCTION_ERROR;
add_scalar_to_vector(list, length, scalar);
if (!MLNewPacket(link)) return LIBRARY_FUNCTION_ERROR;
if (!MLPutReal64List(link, list, length)) return LIBRARY_FUNCTION_ERROR;
return LIBRARY_NO_ERROR;
}
The implementation of the universal wrapper function is straight forward. It uses the MathLink API to read the a vector and a scalar from the underlying link and also writes back the result on the link. A std::unique_ptr with a user-supplied deleter lambda function ensures that the vector read with MLGetReal64List
is cleaned up automatically with MLReleaseReal64List
when the function returns.
The LibraryLink MLINK
signature wrapper function must be loaded into the Mathematica kernel session with a slightly different command:
With[{func=LibraryFunctionLoad["AddScalarToVector",
"AddScalarToVectorLL", LinkObject, LinkObject]},
AddScalarToVectorLL[vector: {_Real ...}, scalar_Real] := func[vector, scalar]
]
LibraryFunctionLoad
now uses the Mathematica expression LinkObject
as a data type, which corresponds to MLINK
on the C++ side. To ensure that the native function is invoked with the correct arguments (a vector and a scalar), we setup a regular Mathematica function pattern which calls the native function loaded from the dynamic library.
To use the universal wrapper function from the MathLink executable, we have to make some changes to the MathLink template file we presented in the first part:
:Begin:
:Function: AddScalarToVectorML
:Pattern: AddScalarToVectorML[vector: {_Real ...}, scalar_Real]
:Arguments: { vector, scalar }
:ArgumentTypes: Manual
:ReturnType: Manual
:End:
void AddScalarToVectorML(void) {
if (AddScalarToVector(stdlink) != 0) {
MLNewPacket(stdlink);
MLPutSymbol(stdlink, "$Failed");
}
}
In the data type mapping we now declare the :ArgumentTypes:
as Manual
, because the universal wrapper function takes care of reading all the arguments from the link. The MathLink wrapper function AddScalarToVectorML
simply invokes the universal wrapper with the underlying MathLink connection stdlink
. To make MathLink error handling work correctly, we have to return a $Failed
symbol if the universal wrapper returns an error.
We wish to improve ourselves
When a new C++ function needs to be integrated into the Wolfram Language, the LibraryLink and MathLink wrapper functions can be copied almost verbatim. Development effort only goes into writing a new universal wrapper function.
What concerns the amount of code we had to write, for the simple function add_scalar_to_vector
it does make much of a difference. However, if you have to integrate dozens of C++ functions with long parameter lists, as is the case with UnRisk-Q, a great deal of tedious work can be saved with a universal wrapper function.
Show me the code
You can download a self-contained CMake project which demonstrates how to build a MathLink executable and a LibraryLink dynamic library using a universal wrapper function. You need the following third-party software packages:
- Mathematica ≥ 8
- CMake ≥ 2.8.9
- Clang ≥ 3.1 or GCC ≥ 4.6 or Visual Studio C++ ≥ 2010
The file CMakeLists.txt
in the zip archive contains instructions on how to build and run the tests for Windows, Linux and OS X.
Performance
We’ll now evaluate the performance of the different linking technologies presented in this blog episode:
- MathLink using the universal wrapper function
MLINK
based LibraryLink using the universal wrapper functionMArgument
based LibraryLink with the custom wrapper function from part one
We’ll be using the following pseudo test code:
Load external function
Call add_scalar_to_vector 100 times with a list of 10^6 random double values
Unload external function
The actual test code for the different linking technologies can be seen in the file CMakeLists.txt
file in the zip archive.
The following chart shows the execution times of the different linking technologies. The results were obtained with Mathematica 10 on a MacBook Pro Mid 2010 running OS X 10.9. The code was compiled with Clang 3.4. The results for Linux and Windows are similar.
Summing up, we can draw the following conclusions, what concerns the communication overhead for integrating a native C++ function into the Wolfram Language:
- Using a universal wrapper function, you a get a speedup of about 50 percent by moving from MathLink to LibraryLink with almost no additional development effort.
- An additional speedup of about 50 percent is possible, if you go to the extra effort of writing an
MArgument
based wrapper function.