使用Bindgen为ELK生成Rust绑定


介绍

bindgen 是一个能自动为 C(或 C++)库生成 Rust 绑定的辅助库和命令行工具。

elk 是一个迷你的JS引擎.
能够实现类似于这样的效果

main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include "elk.h"

// C function that adds two numbers. Will be called from JS
jsval_t sum(struct js *js, jsval_t *args, int nargs) {
if (nargs != 2) return js_err(js, "2 args expected");
double a = js_getnum(args[0]); // Fetch 1st arg
double b = js_getnum(args[1]); // Fetch 2nd arg
return js_mknum(a + b);
}

int main(void) {
char mem[200];
struct js *js = js_create(mem, sizeof(mem)); // Create JS instance
js_set(js, js_glob(js), "sum", js_mkfun(sum))); // Import sum()
jsval_t result = js_eval(js, "sum(3, 4);", ~0); // Call sum
printf("result: %s\n", js_str(js, result)); // result: 7
return 0;
}

如果这个执行内容来自于服务器下发,那就可以很方便地动态下发程序然后执行特定的任务.

之前在玩ESP-IDF时候就尝试内嵌一个Lua引擎来动态执行Lua脚本的操作,然后用蓝牙更新Lua脚本以实现动态的绘制界面.
最近弄ESP-RS时候想试试类似的效果,因为感觉Lua用起来很啰嗦,现在已经完全忘记如何编写Lua脚本了.
当然Rust也有一些Lua引擎的现成crates,比如rLua.
但是数组索引从1开始真的是坏文明啊, 我们还是继续捣鼓js吧.

创建Rust项目

1
cargo new "bindgen_elk"

克隆elk源码

进入刚才创建的目录后,在src同级目录下克隆elk.

1
2
3
4
5
6
7
8
9
cd bindgen_elk
git clone https://github.com/cesanta/elk.git
Cloning into 'elk'...
remote: Enumerating objects: 932, done.
remote: Counting objects: 100% (204/204), done.
remote: Compressing objects: 100% (101/101), done.
remote: Total 932 (delta 95), reused 152 (delta 87), pack-reused 728 (from 1)
Receiving objects: 100% (932/932), 4.66 MiB | 12.09 MiB/s, done.
Resolving deltas: 100% (442/442), done.

配置依赖

在当前项目的Cargo.toml中添加bindgencc依赖.

Cargo.toml
1
2
3
4
5
6
7
8
9
10
11
[package]
name = "bindgen_elk"
version = "0.1.0"
edition = "2024"

[dependencies]
libc = "0.2"

[build-dependencies]
cc = "1.0"
bindgen = "0.69"

创建wrapper.h

根据bindgen的规则,需要在项目目录下创建一个叫做wrapper.h的头文件,并在该文件内引入想要绑定的库头文件.

wrapper.h
1
#include "elk/elk.h"

编写build.rs

在src同级别目录下创建一个名为build.rs的文件, 当存在build.rs文件时候, Cargo会优先编译执行该文件.

请注意指定include目录, 我用的是mingw64, 所以在bindgen::Builder::default()时候,手动指定了target和sysroot

1
2
3
4
5
6
7
8
9
10
11
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
// 1. 指定目标三元组
.clang_arg("--target=x86_64-w64-mingw32")
// 2. 指定 MinGW 的根目录
.clang_arg(format!("--sysroot={}", "C:/Program Files/mingw64"))
// 3. 显式添加必要的 GCC 内部路径 (如果上述两项还不够)
.clang_arg("-IC:/Program Files/mingw64/lib/gcc/x86_64-w64-mingw32/8.1.0/include")
.generate()
.expect("Unable to generate bindings");

内容如下,具体步骤含义见注释.

build.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
use std::{
env,
path::{Path, PathBuf},
};

extern crate cc;
/**
* 编译出lib文件
*/
fn compile_libelk() {
// 定义源文件路径
let src = ["elk/elk.c"];
// 创建cc builder
let mut builder = cc::Build::new();
/*
* files 源文件们
* include 头文件路径
* flag -DJS_DUMP宏用于打印js调试信息. 见elk.h内js_dump函数注释
*/
let build = builder.files(src.iter()).include("elk").flag("-DJS_DUMP");
build.compile("elk");
}

/**
* 生成binding.rs
*/
fn bindgen_generate() {
// 获取当前Cargo.toml所在文件夹,一般来说就是该项目位置
let dir = env::var("CARGO_MANIFEST_DIR").unwrap();
// 指定库路径,即elk文件夹
println!(
"cargo:rustc-link-search=native={}",
Path::new(&dir).join("elk").display()
);
// 如果 wrapper.h 文件发生了变化, 就重新运行构建脚本
println!("cargo:rerun-if-changed=wrapper.h");
// 配置绑定
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
// 1. 指定目标三元组
.clang_arg("--target=x86_64-w64-mingw32")
// 2. 指定 MinGW 的根目录
.clang_arg(format!("--sysroot={}", "C:/Program Files/mingw64"))
// 3. 显式添加必要的 GCC 内部路径 (如果上述两项还不够)
.clang_arg("-IC:/Program Files/mingw64/lib/gcc/x86_64-w64-mingw32/8.1.0/include")
.generate()
.expect("Unable to generate bindings");

let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
// 生成文件写入到binding.rs
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}

fn main() {
compile_libelk();
bindgen_generate();
}

生成并使用binding.rs

如果你的环境正常,这时候只需要执行cargo build即可

1
2
3
4
PS E:\GitHub\bindgen_elk> cargo build
# ...忽略大量无用信息
Compiling bindgen_elk v0.1.0 (E:\GitHub\bindgen_elk)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.25s

最终在target\debug\build\bindgen_elk-b6aa022ece64a1fa\out\bindings.rs下找到生成绑定文件.

测试JS脚本

参照文章开头的C调用JS代码, 我们写出Rust版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use std::ffi::{CString, CStr};

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

#[unsafe(no_mangle)]
pub unsafe extern "C" fn sum(js: *mut js, args: *mut jsval_t, nargs: i32) -> jsval_t {
if nargs != 2 {
let msg = CString::new("2 args expected").unwrap();
return js_mkerr(js, msg.as_ptr());
}

let a = js_getnum(*args.offset(0));
let b = js_getnum(*args.offset(1));

js_mknum(a + b)
}

fn main() {
unsafe {
let mut mem = [0u8; 8192];
let js = js_create(mem.as_mut_ptr() as *mut _, mem.len());

let name = CString::new("sum").unwrap();
js_set(js, js_glob(js), name.as_ptr(), js_mkfun(Some(sum)));

let code = CString::new("sum(3, 4);").unwrap();
let code_len = code.as_bytes().len();

let result = js_eval(js, code.as_ptr(), code_len);

let s = CStr::from_ptr(js_str(js, result)).to_str().unwrap();
println!("result: {}", s);
}
}

执行以后可以看到输出

1
2
3
4
PS E:\GitHub\bindgen_elk> cargo run 
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.94s
Running `target\debug\bindgen_elk.exe`
result: 7

环境

  • rustup 1.28.2 (e4f3ad6f8 2025-04-28)
  • cargo 1.89.0-nightly (fc1518ef0 2025-06-06)
  • elk @ a9bb856

参考资料

作者

Chaos Goo

发布于

2025-12-05

更新于

2025-12-05

许可协议