Mathematica 14.0 载入C/C++动态库
在上篇博文中,我们看到,在Mathematica 13.1开始,随着 LibraryFunctionDeclaration
系列的函数的引入,通过mma的编译系统,可以直接将现有的动态库里的函数编译成mma里面的函数,进而直接引用。
然而,在Mathematica 13.3开始(我们这里用14.0),我们有了更便捷的方案,ForeignFunctionLoad
等一系列函数,不用通过编译系统,而可应用动态库的函数。具体文档可见这里,我们下面仅举一个非常简单的例子。
首先我们需要找一个动态库,这里我们在mma安装文件夹的SystemFiles/Libraries
下找到一个叫flint的动态库,由于这个位置不是mma默认的库位置,所以手动找一下:
libflint = With[{dic =
FileNameJoin[{$InstallationDirectory, "SystemFiles",
"Libraries"}]},
FileNameJoin[{dic,
First@Select[Keys[FileSystemMap[FileDate, dic, {2}, 1]],
StringContainsQ[#, "flint"] &]}]]
FLINT(Fast Library for Number Theory)是一个用于数论计算的 C 库,mma内置了他用来进行一些计算。这里我们以下面这些函数为例:
void fmpz_init(fmpz_t f);
void fmpz_clear(fmpz_t f);
void fmpz_set_si(fmpz_t f, slong val);
void fmpz_pow_ui(fmpz_t f, const fmpz_t g, ulong exp);
void fmpz_nextprime(fmpz_t res, const fmpz_t n, int proved);
char * fmpz_get_str(char * str, int b, const fmpz_t f);
这里的slong
是有符号机器整数的别名(在64位机器上就是从 -2^63 到 2^63-1),而ulong
是无符号机器整数。此外还有一个FLINT内置的类型fmpz_t
,它用来表示一个任意精度的整数,他的定义需要翻头文件,位于flint.h
中,一些解释也可见文档 fmpz文档,我们看到
typedef slong fmpz;
typedef fmpz fmpz_t[1];
所以fmpz
就是有符号机器整数,而fmpz_t
则是长度为 1 的fmpz
的一个数组。如果我们想要的整数不能被一个有符号机器整数表示,fmpz_t
会在其地址右移的两位的地方定义一个GMP中定义的任意精度整数mpz_ptr
.
fmpz_init
就是初始化一个fmpz_t
,而fmpz_clear
就是清理内存,这里他本身的长度为 1 的fmpz
的一个数组其实不用管,主要就是如果他在地址右移的两位的地方定义了一个mpz_ptr
,那么我们需要清理一下。
剩下的函数为:
fmpz_set_si
将f
设为有符号整数val
;fmpz_pow_ui
将f
设为g^exp
, 这里exp
是一个无符号整数;fmpz_nextprime
吃进去一个整数n
,给出比他大的一个素数,存进res
中,整数proved
如果非零则会检查返回的是否是真正的素数,一般取 0 就可以了。fmpz_get_str
输入一个fmpz_t
,以 b 进制输出一个字符串,这里第一个参数的字符串用来框定输出的字符串能占多少内存,如果设为NULL
则让系统自行决定。
明白了函数和函数的类型,我们现在可以开始写接口了。首先通过ForeignFunctionLoad
定义一族函数
prefmpzinit = ForeignFunctionLoad[libflint, "fmpz_init",
{"RawPointer"::["Integer64"]} -> "Void"
];
prefmpzclear = ForeignFunctionLoad[libflint, "fmpz_clear",
{"RawPointer"::["Integer64"]} -> "Void"
];
prefmpzsetsi = ForeignFunctionLoad[libflint, "fmpz_set_si",
{"RawPointer"::["Integer64"], "Integer64"} -> "Void"
];
fmpzinit[n_] :=
With[{hh = UnmanageObject@RawMemoryAllocate["Integer64", 1]},
prefmpzinit[hh]; prefmpzsetsi[hh, n]; hh]
fmpzclear[ptr_] :=
With[{hh = 1}, prefmpzclear[ptr]; RawMemoryFree[ptr];]
nullptr = OpaqueRawPointer[0];
fmpznextprime =
ForeignFunctionLoad[libflint,
"fmpz_nextprime", {"RawPointer"::["Integer64"],
"RawPointer"::["Integer64"], "Integer64"} -> "Void"];
fmpzpowui = ForeignFunctionLoad[libflint, "fmpz_pow_ui",
{"RawPointer"::["Integer64"], "RawPointer"::["Integer64"],
"Integer64"} -> "Void"];
fmpzgetstr = ForeignFunctionLoad[libflint, "fmpz_get_str",
{"RawPointer"::["CSignedChar"], "CInt",
"RawPointer"::["Integer64"]} -> "RawPointer"::["CSignedChar"]
];
从语句来看意义都是清楚的,ForeignFunctionLoad
的第一个参数是动态库的位置,第二个参数是动态库中的函数的符号名,后面是函数的定义。这里我们定义了一个空指针nullptr
要扔给fmpzgetstr
. 此外,这里用了UnmanageObject
了,即不让mma来管理这些内存,我们可以手动管理这些内存。
现在,我们首先定义两个指针,表示整数 1 和整数 88888,
res = fmpzinit[1];
n = fmpzinit[88888];
接着我们调用上面这些函数
fmpzpowui[res, n, 280]; // AbsoluteTiming
fmpznextprime[res, res, 0]; // AbsoluteTiming
bignum1 =
RawMemoryImport[fmpzgetstr[nullptr, 10, res], "String"] //
ToExpression;
bignum1 // N
(* Output: {0.0000625, Null} *)
(* Output: {1.24444, Null} *)
(* Output: 4.743268225314390*10^1385 *)
我们就是算了一下 88888^280 的下一个素数,可以用mma自带的函数来检查一下
tmp = 88888^280; // AbsoluteTiming
bignum2 = tmp // NextPrime; // AbsoluteTiming
bignum1 == bignum2
(* Output: {0.0000834, Null} *)
(* Output: {2.77459, Null} *)
(* Output: True *)
两者得到了一样的结果,但是可以注意到 FLINT 只用了一半的时间!
最后,别忘了清理一下内存,
fmpzclear[n];
fmpzclear[res];
Clear[n, res];
这就给出了一个很简单的直接应用动态库中函数的例子。这里的fmpz_t
尚且是一个简单的结构,如果是一个更复杂的结构,这将使得在ForeignFunctionLoad
中写函数定义变得更加复杂,但是本质上和我们上面的例子并没有什么区别。我们期待在未来的某一天,某一个mma的版本,可以直接读或者半自动地读C的头文件,进而可以更容易地导入动态库中的函数。