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_sif设为有符号整数val
  • fmpz_pow_uif设为 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的头文件,进而可以更容易地导入动态库中的函数。

Buwai Lee

Buwai Lee

交换图都不会画的魔法师