Mathematica 13.1 载入C/C++动态库

众所周知,Mathematica(下面简称mma)作为一款巨贵的数学软件,具有相当多眼花缭乱,一辈子也用不到的功能,与此同时,作为一门编程语言,也是设计得颇为精致。但其性能,即使是纯数值计算的性能,也往往不尽如人意。

作为一门动态语言,由于大量函数在设计上就允许大量类型的输入,可以想象,其本身的性能也相当受限。所以,如果可以限制输入类型,像静态语言一般,编译得到更高效的二进制代码,则性能就会有一个比较大的提升。在这方面,自mma 2.0开始,mma引入了 Compile 来实现这点,但即使编译,性能也没有特别大的提升。随着mma 12.0的发布,十个大版本后,许多内置的数据结构、编译器相关的 FunctionCompile 等内置函数的引入和强化,并且结合了编译器后端LLVM,极大地强化了这方面的功能。随后,mma 13.0以及现在的13.1都在这方面有所改进。可以预见,终有一天,可以使用mma直接写出类似于C/C++这样非常高效的代码。

但在这时代来临之前,大家更常见的做法是,将计算中最繁复和耗时的部分交托给其他性能更强的语言,与mma强大的符号计算、模式匹配等结合起来,达到更为高效的工作效率。通过传统的输入输出去结合姑且不论,在结合中最高效的无异于,通过链接库的方式,成为mma的一部分,而这个库是通过一些更高效的语言,最自然的当然就是C/C++,编译而成的。自mma 8.0开始,这类事情有了一个比较统一的方式,即通过 LibraryLink 了一种方式,在C/C++端,引入mma的头文件,生成可以适用于mma的库文件。但是,了解 LibraryLink 的接口、数据类型,对于大部分人,比如不甚熟悉mma和C/C++的人来说,都是极大的负担,为此,许多辅助项目如 LibraryLinkUtilitieswll_interface 等应运而生。

但自mma 13.1开始,随着 LibraryFunctionDeclaration 系列的函数的引入,为mma编写库便变得更为简洁:我们不必在C/C++测操心mma的任何事情了,不用引入mma的header文件,只需要老老实实写我们的核心代码,将其编译成动态库;随后,mma便可以载入这个动态库,通过 LibraryFunctionDeclaration 将其中的函数翻译成mma可以使用的函数,以往这需要我们使用 LibraryLink 在C/C++写许多难以形容的接口。这无疑是为我们的心智极大地减了负!

下面我们写一个最简单的例子(环境为windows 11,编译套件为msvc):在C++侧,编写如下的代码,文件名为mylib.c++

#define DllExport __declspec(dllexport)

extern "C" DllExport int myadd(int a, int b);
extern "C" DllExport int mytotal(int *a, int b);

int myadd(int a,int b){
	return a+b;
}

int mytotal(int *a,int len){
	int tt = 0;
	for (auto i = 0; i < len; i++)
		tt += a[i];
	return tt;
}

myadd 实现了将两个整数相加,而 mytotal 实现了将一个整数数组的元素全部求和,

__declspec(dllexport) 为msvc的自有关键字,用以声明导出函数供外面调用,这里并不重要,在Linux环境下也是类似的。

接着,我们调用msvc进行编译,这里的编译命令为 cl /LD mylib.cpp ,便可以得到 mylib.dll ,其完整路径我们记作 dir/mylib.dll,这就是我们需要的库文件。可以看到,到这一步,我们完全没有触及mma.

下面,我们来到mma侧:

(* load mylib.dll *)
lib = "dir/mylib.dll";
LibraryLoad[lib];

(* declare functions *)
decadd = LibraryFunctionDeclaration[
   "myadd", {"CInt", "CInt"} -> "CInt"];
dectotal = LibraryFunctionDeclaration[
   "mytotal", {"CArray"::["CInt"], "CInt"} -> "CInt"];

(* define function in mma side *)
myadd = FunctionCompile[decadd, 
   Function[{Typed[a, "CInt"], Typed[b, "CInt"]},
    LibraryFunction["myadd"][a, b]]];
    
mytotal = 
  FunctionCompile[dectotal, 
   Function[{Typed[a, "ListVector"::["CInt"]], Typed[b, "CInt"]},
    LibraryFunction["mytotal"][
     CreateTypeInstance["CArray"::["CInt"], a], b]]];

(* unload mylib.dll *)
LibraryUnload[lib];
ClearAll[myadd, mytotal];

先不解释具体的语句,我们来看运行的结果:

In[1] := myadd[11, 22]
Out[1] = 33

In[2] := mytotal[{1, 2, 3, 7}, 4]
Out[2] = 13

可以看到,我们成功地调用了我们上面使用C++实现的动态库!

现在,让我们回头看我们在mma里做了什么。首先,我们载入了动态库,这步是平凡的。随后,我们使用了 LibraryFunctionDeclaration 来声明我们需要调用的函数,注意,我们这里可以直接声明基本的C类型,而非mma的类型,这里,我们甚至可以用 TypeDeclaration 去构造更复杂的C方面的类型。在这步之后,mma就已经定位到动态库中具体函数的位置了。最后一步,就是要编译成为mma的函数,即,我们要转换数据类型,并且适当打包成mma的函数格式方便下一步使用。而在这两步中,往往最困难的是转换数据类型,对一般的数据结构来说,这可能会相当复杂。比如在 mytotal,我们利用了 CreateTypeInstance 来将mma的一个整数数列编程了一个C类型的整数数列,对于其他比较简单的数据类型,Cast 也是一个非常好用的函数。

所以,自13.1引入 LibraryFunctionDeclaration 以来,为mma编写和载入一个动态库将变得如此简单,我们只需要小心地数据类型转换,即可再mma侧直接完成动态库函数的声明和调用。本文便不再深入讨论更复杂的例子,但他们背后的逻辑是相似的。在13.1版本更新公告中,他们演示了如何调用 openssl 库中的RAND_bytes函数,可供参考。

Buwai Lee

Buwai Lee

交换图都不会画的魔法师