Rust FFI 实战:手把手带你为迷你 JS 引擎 ELK 生成绑定(含 Windows MinGW 避坑指南

9 阅读5分钟

本文首发于我的个人博客:chaosgoo.com,欢迎来访交流。

介绍

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

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

##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项目

cargo new "bindgen_elk"

克隆elk源码

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

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依赖.

[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的头文件,并在该文件内引入想要绑定的库头文件.

##include "elk/elk.h"

编写build.rs

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

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

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");

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

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即可

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版本

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);
    }
}

执行以后可以看到输出

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

后记: 如果你对 Rust 在嵌入式领域的更多玩法(如 ESP-RS、自定义渲染引擎等)感兴趣,欢迎访问我的个人博客 chaosgoo.com,我会定期分享更多 DIY 项目的深度笔记。

参考资料