使用 Wolfram 编译器调用外部库

引言

许多编译语言(如 C、C++、Rust、Swift、Haskell 等)可以编译与 C 兼容的动态库. 这些库中的函数可以在编译后的 Wolfram 语言代码中直接调用,从而可以编写顶级 Wolfram 语言和动态库之间的高性能链接.

本教程包含一个简短示例,是关于与单个 OpenSSL 函数的连接,以及一个扩展示例,介绍与一大块 SQLite 功能的接口.

连接到 OpenSSL

OpenSSL 把实现加密功能的 C 兼容库公开. 此示例将演示与 OpenSSL 的加密安全伪随机数生成器的连接.

OpenSSL 公开一个名为 RAND_bytes 的函数,其类型签名如下:

int RAND_bytes(unsigned char *buf, int num);

第一个参数 buf 是将随机字节写入其中的缓冲区. 第二个参数 num 是要写入 buf 的字节数.

此函数声明可以用以下 LibraryFunctionDeclaration 表示:

(不需要指定库路径,因为 OpenSSL 已经由 Wolfram 语言内核默认加载.)

使用此声明编译函数:

前面的函数使用几个概念来工作.

首先,CreateTypeInstance 用于创建具有给定长度的托管 "CArray". 这是将写入随机字节的缓冲区.

其次,LibraryFunction["RAND_bytes"] 用于调用 LibraryFunctionDeclaration 声明的 OpenSSL 函数.

最后,CreateTypeInstance 再次用于提取缓冲区的元素,并将它们复制到 "NumericArray". FromRawPointer 也可用于此任务.

编译完所有内容后,运行该函数以获取 10 个加密安全伪随机字节:

连接到 SQLite

SQLite 是实现 SQL 数据库的 C 兼容库. 为了运行本教程,必须将 SQLite 作为数据包下载.

运行此命令以下载 SQLite:

可以通过运行 PacletUninstall 来卸载数据包.

SQLite 记录的 C 接口包含一个名为 sqlite3_libversion_number 的函数,其签名如下:

int sqlite3_libversion_number(void);
此 C 签名可以用以下 LibraryFunctionDeclaration 表示:
使用 Wolfram 编译器通过库函数编译程序:

这里演示的是调用单个函数,但同样的机制可用于创建复杂的链接,这些链接可以将复杂的自定义数据结构传入和传出动态库.

本教程详细介绍在高级 Wolfram 语言和 SQLite 之间创建有效链接所需的所有步骤. 首先,创建一个系统,用于打开和自动关闭 SQLite 数据库. 然后,在这些数据库上执行查询. 最后,从查询中读回结果,并将其转换为适用于进一步数据分析的 Wolfram 语言表达式.

打开和关闭数据库

连接到 SQLite 数据库,第一步是打开和关闭它. 这是通过两个函数完成的,它们具有以下签名:

int sqlite3_open(
const char *filename, /* Database filename (UTF-8) */
sqlite3 **ppDb /* OUT: SQLite db handle */
);

int sqlite3_close(sqlite3*);
这可以翻译成下面 LibraryFunctionDeclarations 的例子,并具有 sqlite3* 类型的别名:
使用这些,编译一个简单的函数来打开和关闭给定路径的数据库:
在 SQLite 数据库的路径上执行此函数会返回错误代码(SQLITE_OK):
在之前的程序中,数据库当不再需要时需要手动关闭. 通过使用 "Managed" 类型添加自动内存管理,可以简化流程,使得数据库在超出范围时自动关闭:
打开现有的 SQLite 数据库并将其作为内存管理对象返回:

"Managed" 对象在编译代码和顶层代码中都有一个共享的引用计数,当引用计数变为 0 时,将执行释放代码. 这提供了一种在顶层传递 SQLite 数据库对象的内存安全方式.

简单查询

现在可以打开和(自动)关闭数据库,可以在上面运行查询. SQLite 最简单的查询接口是通过 sqlite3_exec

int sqlite3_exec(
sqlite3*, /* An open database */
const char *sql, /* SQL to be evaluated */
int (*callback)(void*,int,char**,char**), /* Callback function */
void *, /* 1st argument to callback */
char **errmsg /* Error msg written here */
);
忽略回调和错误消息接口,sqlite3_exec 可以用以下 LibraryFunctionDeclaration 表示:
编译一个接受数据库对象(因为它将从先前定义的 openDB 函数返回)的函数并在其上运行查询:
如要试用,为数据库创建一个临时文件:
打开数据库:
执行查询以在新数据库中创建表:

返回错误码 0,表示表创建成功.

再次运行相同的查询返回错误代码 1,表示查询失败,因为表已经存在:

这个 execquery 函数可以在数据库上运行任意查询,但它没有读取查询结果或以有用的形式返回它们的机制. 这将是下一节的主题.

添加读取功能

遍历查询结果

为了从查询结果中读取,使用了与之前略有不同的界面。 不是使用 sqlite3_exec,而是使用 sqlite3_prepare 来生成查询语句,并使用 sqlite3_step 逐步执行结果. 为了关闭查询语句,还需要使用 sqlite3_finalize. 它们的类型签名是:

int sqlite3_prepare(
sqlite3 *db, /* Database handle */
const char *zSql, /* SQL statement, UTF-8 encoded */
int nByte, /* Maximum length of zSql in bytes. */
sqlite3_stmt **ppStmt, /* OUT: Statement handle */
const char **pzTail /* OUT: Pointer to unused portion of zSql */
);

int sqlite3_step(sqlite3_stmt*);

int sqlite3_finalize(sqlite3_stmt *pStmt);
将这些签名转换为 LibraryFunctionDeclarations,并对 sqlite3_stmt 指针使用 TypeDeclaration
声明一个编译查询并返回内存管理语句对象的函数:
编译一个接受查询语句对象的函数,遍历其结果,并返回结果个数:
打开一个示例数据库,并计算客户表的行数:

将行变成表达式

上面的函数使得执行查询和遍历结果成为可能. 但是,读取每一行数据并将其转换为 Wolfram 语言表达式需要更多的库函数. 特别地,以下这些 SQLite 函数用于获取列数、名称和类型:

int sqlite3_column_count(sqlite3_stmt *pStmt);

const char *sqlite3_column_name(sqlite3_stmt*, int N);

int sqlite3_column_type(sqlite3_stmt*, int iCol);
这可以用以下 LibraryFunctionDeclarations 表示:

这些 SQLite 函数用于从每一行中提取各种类型的数据:

double sqlite3_column_double(sqlite3_stmt*, int iCol);

sqlite3_int64 sqlite3_column_int64(sqlite3_stmt*, int iCol);

const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol);
它们可以用以下附加的 LibraryFunctionDeclarations 表示:
接下来声明一个函数,该函数接受一个语句对象和一个列号,并将列的值作为表达式返回. sqlite3_column_type 用于获取列的类型,然后调用相应的函数来获取值:
最后,声明一个函数,从查询结果的给定行中提取所有列:

编译查询函数

现在可以使用 openDB 打开(并自动关闭)数据库,可以使用 createStatement 编译查询,并且可以使用 rowAssociation 将查询结果的每一行转换为表达式. 剩下的就是把它放在一起.

将所有内容编译成一个函数,该函数接受一个 SQLite 数据库和一个查询字符串,并将结果作为关联列表返回:
打开一个 SQLite 数据库:
查询它:
执行更大的查询并将结果放入 Dataset 中:

完整代码

一次性调用 FunctionCompile 的完整代码: