跳至内容
返回

使用Bindgen为ELK生成Rust绑定

发布于:  at  10:57 上午

介绍

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

环境

参考资料


在以下平台分享此文章: