跨语言函数调用 FFI 入门
一、跨语言调用函数
gRPC 跨服务器调用函数,非常熟悉,静态介绍知识点是跨语言调用函数 FFI
。
常见的块语言编程技术:
- ffi
- wasm
- 语言解释器和虚拟机(jvm)
- 桥接
- api/rpc
- 其他
二、什么是 FFI ?
全称:Foreign Function Interface,可以简单的理解为外部语言函数接口
,一种编程语言为另外一种编程语言提供函数函数,他们拥有相同的函数接口。
FFI 这个术语最早来自 Common Lisp [规范。目前几乎所有严肃编程的语言都有提供 FFI 的支持,但大多数是单向功能。
FFI 典型的使用场景就是:在一个高级解释性语言如 Python 中调用原生代码语言函数,例如:调用二进制动态链接库的上下文中。
FFI 是与前辈写下代码, 交互的神器。
三、FFI 实现原理
参数传递 + 函数调用 + 返回值传递。通常实现一个FFI, 需要考虑一起一些因素:
- 动态链接库加载
- 函数调用约定
- 参数转换
- 内存管理
- 错误处理
- ...
当然实现 FFI 困难存在不同程度的难度,有些像 Rust 没有 GC 的语言实现起来与含 GC 有差异,需要注意。其次不同的语言的数据类型,语言特点也不同,Rust/Go/C++ 等使用结构体数据解构,存在很大的不同。
node-ffi 一个 node-ffi 的基于 c++ 的实现,需要知道原理的可以了解一下。 但是这里不推荐使用了,因为随着 Node.js 的发展,这些库已经很长时间没有更新了。
四、Node.js FFI
Node.js 的 FFI使用 ffi-rs,一个用 Rust 编写的 Node.js FFI 工具库。下面我们尝试一个简单的 demo。开始之前,我们希望你已经有了:
- cargo (打包 rust 库包含
.dll
文件) - node.js(基于 pnpm 和 esm)
cargo new --lib # 创建一个新的库
pnpm init # 创建一个 pnpm 项目
pnpm add ffi-rs
mkdir app
cd app & touch index.mjs
并且在 package.json 中添加 "type": "module"
,我们将在 esm 中运行 Node.js 程序。
4.1)rs 代码并打包为 dll 库
配置:
[lib]
crate-type = ["cdylib"]
rust 代码:
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
pub
关键字表示这个函数是公开的,可以被其他模块访问到。extern "C"
表示这个函数使用 C 语言的调用约定,这意味着函数的名字不会被 Rust 编译器修改,可以被 C 代码调用。
4.2)使用 cargo 构建出 dll 库
cargo build --release
构建的库的位置:/target/release/xxx.dll
文件。
4.3)node.js 中使用
import * as pkg from 'ffi-rs'
const { open, load, DataType, } = pkg;
const a = 1
const b = 100
open({
library: 'libadd', // key
path: './target/release/ffi_test_lib.dll' // 注意此处是运行时是根目录
})
const r = load({
library: "libadd", // 库名
funcName: 'add', // 函数名
retType: DataType.I32, // 返回值
paramsType: [DataType.I32, DataType.I32], // 参数类型
paramsValue: [a, b] // 参数值
})
console.log(r) // 101
运行 node ./app/index.mjs
程序,最后得出的结果符合预期。
4.4)小结
ffi-rs 调用 dll 文件也非常简单,使用 open 函数打开,使用 load 函数读取,并获取返回值,需要注意的是函数的参数和返回值等配置问题需要注意。
五、Deno FFI
- Deno FFI 使用也非常简单,需要安装额外的库支持:
const libName = `./libadd.${libSuffix}`;
// Open library and define exported symbols
const dylib = Deno.dlopen(
libName,
{
"add": { parameters: ["isize", "isize"], result: "isize" },
} as const,
);
// Call the symbol `add`
const result = dylib.symbols.add(35, 34); // 69
- Deno.dlopen 安装文件打开。
- dylib.symbols.add 调用即可。
六、Bun FFI
- Bun FFI 也默认支持了 ffi,使用起来也非常简单,甚至给你把不同的平台的后缀也做了:
import { dlopen, FFIType, suffix } from "bun:ffi";
const path = `libsqlite3.${suffix}`;
const {
symbols: {
sqlite3_libversion, // the function to call
},
} = dlopen(
path, // a library name or file path
{
sqlite3_libversion: {
// no arguments, returns a string
args: [],
returns: FFIType.cstring,
},
},
);
console.log(`SQLite 3 version: ${sqlite3_libversion()}`);
与 deno 类似,需要 dlopen 调用,然后 symbols 上挂载了目标方式,我们使用只需要在 symbols 上调用即可。
七、其他语言
八、小结
本文主要讲解了 Node.js 和 JavaScript 生态和 Rust 生态中对 FFI 的支持,以及基本使用方法,总体使用还是比较简单的。FFI 使用一种跨语言的技术,使用用于高级语言调用底层的动态库等情况。例如在 electron 项目中需要调用基础库中内容,可以直接使用 Node.js ffi 能力调用,实现桌面端能力,扩展 Node.js 能力。
转载自:https://juejin.cn/post/7363503456538738707