相关文章推荐
爱运动的手电筒  ·  RDS ...·  2 月前    · 
善良的煎饼果子  ·  比Microsoft ...·  1 年前    · 

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]


抛出异常

参见 Error Handling - Wiki

C++

#include "wll_interface.h"
int int_div(int x, int y)
    if (y == 0)