掘金 后端 ( ) • 2024-05-02 08:12

一、跨语言调用函数

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 能力。