wll-interface:轻松调用C++函数
简介
如果要问,什么是在 Mathematica 中调用外部程序性能最好的方法,那么答案必然是 LibraryLink。它本质上是 Mathematica 加载了动态链接库 ( .dll , .so ) 中的函数,然后运行,这使得 LibraryLink 函数调用的开销基本少于 1μs,且运行效率和原生 C++ 相同。
虽然有性能优势,但是写 LibraryLink 函数却会十分枯燥。这主要体现在参数传递和数组操作需要用到很多繁琐的函数。如果你感兴趣的话可以参考 这篇文章 ,为了实现矩阵转置的操作,需要写数十行的 C 代码。
为了简化 LibraryLink 的使用,我写了一个 C++ 头文件库,wll-interface。它能自动处理 LibraryLink 函数的输入输出,并且通过封装使数组操作非常简便。做到了只需要在原生 C++ 函数的基础上添加两行代码,就可以在 Mathematica 中导入并运行了。wll-interface 可以从 GitHub 下载 ( njpipeorgan/wll-interface )。这篇文章会比较简略的介绍它的使用方法,这个项目的 Wiki 中有完整的介绍。
如何使用
在这一节中,我会用一个实际的例子介绍如何使用 wll-interface。在开始之前,首先需要准备好 Mathematica (10.0 或以上),以及一个 支持 C++17 的编译器 (GCC 7+, Clang 4+, MSVC 19.11+, ICC 19.0+)。
在这个例子中,我使用的是 Mathematica 11 和 MSVC 19.14 (Visual Studio 2017 v15.7)。
首先,我们在 VS2017 中创建一个项目
1. 新建一个空项目 MyLibraryLinkProject 。
2. 添加一个源文件 function.cpp ,我们之后会把代码写在这个文件中。
其次,我们设置这个项目的属性
1. 找到 Mathematica 的 "C/C++ IncludeFiles" 文件夹:在 Mathematica 中运行
SystemOpen[$InstallationDirectory<>"\\SystemFiles\\IncludeFiles\\C"]
2. 将 wll_interface.h 下载到这个文件夹中。( Github )
3. 在 VS 中打开: 项目 > MyLibraryLinkProject 属性 。
4. 在页面顶部,选择 All Configuration,x64。
打开右侧的
配置管理器
..., 选择 Debug,x64。
在页面左侧,选择
常规
,将页面中间的
配置类型
更改为 动态库(.dll)。
在页面左侧,选择
C/C++
>
命令行
,在底部的
其他选项
中填入
/std:c++17 /I "[Mathematica 的 C/C++ IncludeFiles 文件夹路径]"
比如,"C:\Program Files\Wolfram Research\Mathematica\11.3" 是 Mathematica 的安装路径,那么填的内容大概是
/std:c++17 /I "C:\Program Files\Wolfram Research\Mathematica\11.3\SystemFiles\IncludeFiles\C"
5. 保存所有的设置。
然后,我们写入代码并编译
在我们之前添加的 function.cpp 中写入以下代码:
#include "wll_interface.h" // 引用 wll_interface
double power(double b, int p) // 定义 "power" 函数
double result = std::pow(b, p); // 计算 b 的 p 次幂
return result; // 返回结果
DEFINE_WLL_FUNCTION(power) // 定义 LibraryLink 函数 "wll_power"
DEFINE_WLL_FUNCTION 是 wll-interface 中定义的一个宏。在上面的例子中它会将函数 "power" 包装为 "wll_power",其中自动添加的 "wll_" 前缀保证了函数的名称不会重复。wll-interface 会在其中通过 LibraryLink 传递参数,同时转换参数类型。
在菜单栏中选择 生成 > 生成解决方案。如果编译成功的话,会在 输出 窗口中显示动态连接库的路径,"...\...\MyLibraryLinkProject.dll",一下简称 库路径 。
最后,加载动态链接库
打开 Mathematica,将 mylib 设为动态链接库的路径:
mylib = "...\\...\\MyLibraryLinkProject.dll";
然后通过 LibraryFunctionLoad 从 mylib 中导入函数 "wll_power"。因为 Mathematica 并不知道我们的函数的参数和返回值是什么类型的,我们要在 LibraryFunctionLoad 中设定参数的类型为 实数 (Real) 和 整数 (Integer),返回值为 实数 (Real):
power = LibraryFunctionLoad[mylib, "wll_power", {Real, Integer}, Real];
现在,我们可以像普通的 Mathematica 函数一样调用 power:
power[3.14, 2] (* gives 9.8596 *)
如果想要更改代码重新编译,则需要先从 Mathematica 中卸载 mylib:
LibraryUnload[mylib];
LibraryLink 的类型系统
wll-interface 支持的类型大致可分为两类: 标量类型 和 数组类型 ,覆盖了 LibraryLink 中常用的类型。
函数的参数类型和返回值需要同时在 C++ 代码和 Mathematica 代码中声明。在 C++ 代码中,函数的类型是通过函数签名来表示的:
ReturnType f(ArgumentTypes...);
而在 Mathematica 代码中,函数的类型是通过 LibraryFunctionLoad 的第三和第四个参数来表示的:
LibraryFunctionLoad[(*库路径*), (*wll_函数名*), {ArgumentTypes...}, ReturnType]
需要注意的是,C++ 和 Mathematica 中表示同一种 (或者可以相互转换的) 类型的写法是不同的,下面我们具体介绍这些类型和它们的表示方法。
标量类型
1. 整数
Mathematica:
Integer
C++ 对应类型:
int32_t
或者
int64_t
(取决与平台类型)
C++ 可类型转换:其他任意整数类型,比如
int
,
char
,
size_t
, ...
2. 布尔值
Mathematica:
"Boolean"
(需要加引号)
C++ 对应类型:
bool
3. 实数
Mathematica:
Real
C++ 对应类型:
double
C++ 可类型转换:
float
,
long double
4. 复数
Mathematica:
Complex
C++ 对应类型:
std::complex<double>
C++ 可类型转换:
std::complex<T>
(
T
为
int
,
float
, ...)
5. 字符串
Mathematica:
"UTF8String"
C++ 对应类型:
std::string
C++ 可类型转换:
const char*
6. 空
Mathematica:
"Void"
C++ 对应类型:
void
数组类型
LibraryLink中的数组包括 常规数组 (数据线性地存储在内存中) 和 稀疏数组 (数据以位置和值的方式存储) 两种。在本文中我们只介绍常规的数组。在 Wiki 中有关于稀疏数组的介绍。常规数组在 wll-interface 中以模板类 tensor 包装。为了避免名称冲突,wll-interface 的类和数组都被放在了命名空间 wll 中。
1. 数据类型和数组阶数
LibraryLink 中的数组有许多属性,而我们在写它们的类型的时候只需要关心 类型 T 和 阶数 R 。数组在 Mathematica 中的写法为: {T,R} ,在 C++ 中的写法为 wll::tensor<T,R> 。例如一个整数矩阵的类型可以表示为: {Integer,2} (Mathematica) 和 wll::tensor<int,2> (C++)。因为列表 (一阶数组) 和矩阵 (二阶列表) 最为常用,这两个类型在 wll-interface 中分别有别名: wll::list<T> 等价于 wll::tensor<T,1> , wll::matrix<T> 等价于 wll::tensor<T,2> 。
在下面这个例子中,我们在 C++ 中声明了函数 sum,并在 Mathematica 中加载这个函数。
函数 sum 接受一个实数的列表作为参数,然后返回一个实数:
double sum(wll::list<double> input);
然后在 Mathematica 中用 LibraryFunctionLoad 加载:
LibraryFunctionLoad[(*库路径*), "wll_sum", { {Real, 1} }, Real]
2. 数组尺寸
在 wll-interface 中,我们可以通过一个整数列表表示数组的尺寸,以此构造一个数组,例如
wll::tensor<int, 3> mytensor({4, 6, 2}); // 4×6×2 的整数数组
我们可以通过 wll::tensor 的 .dimension 方法获取数组的尺寸。比如在上面的例子中 mytensor.dimension(0) 等于 4,mytensor.dimension(2) 等于 2。
3. 数组取值
按找 C/C++ 的惯例,数组的位置是从 0 开始计数的 (而 Wolfram Language 是从 1 开始计数的)。比如 ,我们有一个名为 mylist 的列表,那么 mylist(4) 表示的是它的第五个值;而一个矩阵 mymatrix 在第 i 行第 j 列的值需要用 mymatrix(i-1,j-1) 来表示。
与 C++ 标准库类似,wll::tensor 有 at 方法。mymatrix.at(i,j) 相当于 mymatrix(i,j)。同时,负数也可以用作取值的指标,- i 表示从后往前数的第 i 个位置。
4. 数组初始化
之前我们提到,数组在构造的时候可以用一个整数列表来表示数组的尺寸。wll::tensor 也可以用具体的数据来构造,而不是默认地初始化为零。比如,
wll::list<int> mat3({8}, {3,1,4,1,5,9,2,6} ); // 整数列表
在有数值化数据的情况下,可以将数组的尺寸列表留空,这样 wll-interface 会自动推断尺寸。下面这个例子构造了一个 3×3 的矩阵。
wll::matrix<int> mat3({ }, {{1,0,0}, {0,1,0}, {0,0,1}} );
初始化数据的各个数值必须有相同的类型 (比如全是整数),但是这个类型不一定需要和数组的数据类型完全一致,只需要保证可以进行转换。
范例
数组排序
C++
#include <algorithm>
#include "wll_interface.h"
wll::list<double> sort(wll::list<double> a)
std::sort(a.begin(), a.end());
return a;
DEFINE_WLL_FUNCTION(sort)
Mathematica
sort = LibraryFunctionLoad[(*库路径*), "wll_sort", {{Real, 1}}, {Real, 1}];
生成随机数数组
C++
#include <random>
#include "wll_interface.h"
wll::list<int> uniform_rand(wll::list<int> range, size_t size)
static std::default_random_engine e(std::random_device{}());
std::uniform_int_distribution<int> dist(range(0), range(1));
wll::list<int> result({size});
for (int& element : result)
element = dist(e);
return result;
DEFINE_WLL_FUNCTION(uniform_rand)
Mathematica
rand = LibraryFunctionLoad[(*库路径*), "wll_uniform_rand", {{Integer, 1}, Integer}, {Integer, 1}];
生成 1~6 之间的 10 个随机数
rand[{1, 6}, 10]
抛出异常
C++
#include "wll_interface.h"
int int_div(int x, int y)
if (y == 0)