JS `Deno` `FFI` 高阶:与 Rust `FFI` 结合构建高性能原生模块

各位观众老爷,大家好!我是你们的老朋友,今天给大家带来一场关于 Deno FFI 结合 Rust FFI 构建高性能原生模块的精彩讲座。准备好了吗?咱们开车了!

开场白:为什么要 Deno FFI + Rust?

想象一下,你正在用 Deno 构建一个超酷的应用,但是突然遇到了性能瓶颈。JavaScript 的性能再好,也总有一些场景力不从心,比如图像处理、密码学计算、或者是一些底层系统调用。这时候,你就需要一剂猛药——原生模块。

Deno 提供了 FFI (Foreign Function Interface) 机制,允许你直接调用 C/C++/Rust 等语言编写的动态链接库。而 Rust,作为一门安全、高效的系统级编程语言,简直是原生模块的不二之选。

所以,Deno FFI + Rust,就像是给你的 Deno 应用装上了一台 V8 发动机,让它瞬间起飞!

第一部分:Rust 篇:打造高性能的积木

首先,咱们先来用 Rust 打造一块高性能的积木,也就是我们的动态链接库。

  1. 创建 Rust 项目:

    cargo new --lib deno_rust_lib
    cd deno_rust_lib
  2. 配置 Cargo.toml:

    我们需要告诉 Rust 编译器,我们要构建的是一个动态链接库。在 Cargo.toml 文件中添加以下内容:

    [lib]
    crate-type = ["cdylib"]
    • crate-type = ["cdylib"]:这行代码告诉 Rust 编译器,我们要构建的是一个 C 兼容的动态链接库。
  3. 编写 Rust 代码:

    接下来,我们编写一些简单的 Rust 代码,导出一个函数,比如一个加法函数和一个字符串拼接函数。

    use std::ffi::CString;
    use std::os::raw::c_char;
    
    #[no_mangle]
    pub extern "C" fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    
    #[no_mangle]
    pub extern "C" fn concat(s1: *const c_char, s2: *const c_char) -> *mut c_char {
        unsafe {
            let c_str1 = CString::from_raw(s1 as *mut c_char);
            let c_str2 = CString::from_raw(s2 as *mut c_char);
    
            let str1 = c_str1.into_string().unwrap();
            let str2 = c_str2.into_string().unwrap();
    
            let combined_string = str1 + &str2;
    
            let c_string = CString::new(combined_string).unwrap();
            c_string.into_raw()
        }
    }
    
    #[no_mangle]
    pub extern "C" fn free_string(s: *mut c_char) {
        unsafe {
            if !s.is_null() {
                CString::from_raw(s); // This will free the memory
            }
        }
    }
    • #[no_mangle]:这个属性告诉 Rust 编译器,不要修改函数的名字。我们需要保持函数名不变,以便 Deno 可以通过名字找到它们。
    • pub extern "C":这个声明告诉 Rust 编译器,我们要导出一个 C 兼容的函数。
    • i32, *const c_char, *mut c_char:这些是 C 语言中的数据类型。我们需要使用 C 兼容的数据类型,以便 Deno 可以正确地传递数据。
    • unsafe:Rust 是一门安全的语言,但是当我们与 C 代码交互时,我们需要使用 unsafe 块,因为 C 代码是不安全的。我们需要自己负责内存管理。
    • CString:Rust 的 CString 类型用于表示 C 风格的字符串。
  4. 构建动态链接库:

    运行以下命令构建动态链接库:

    cargo build --release

    构建完成后,你会在 target/release 目录下找到一个 .so (Linux), .dylib (macOS), 或者 .dll (Windows) 文件。这就是我们的动态链接库。

第二部分:Deno 篇:优雅地调用原生模块

现在,我们有了 Rust 编写的动态链接库,接下来,我们需要在 Deno 中调用它。

  1. 准备 Deno 代码:

    创建一个 Deno 文件,比如 deno_app.ts,然后编写以下代码:

    const lib = Deno.dlopen(
      "./target/release/libdeno_rust_lib.dylib", // 根据你的平台修改文件名
      {
        add: { parameters: ["i32", "i32"], result: "i32" },
        concat: { parameters: ["pointer", "pointer"], result: "pointer" },
        free_string: { parameters: ["pointer"], result: "void" }
      },
    );
    
    const result = lib.symbols.add(10, 20);
    console.log("10 + 20 =", result);
    
    const str1 = "Hello, ";
    const str2 = "Deno!";
    
    // Encode strings to UTF-8 and get pointers
    const encoder = new TextEncoder();
    const str1_encoded = encoder.encode(str1 + ''); // Null-terminate the string
    const str2_encoded = encoder.encode(str2 + ''); // Null-terminate the string
    const str1_ptr = Deno.UnsafePointer.of(str1_encoded);
    const str2_ptr = Deno.UnsafePointer.of(str2_encoded);
    
    // Call the concat function
    const combined_ptr = lib.symbols.concat(str1_ptr, str2_ptr) as Deno.UnsafePointer;
    
    // Decode the result string
    const decoder = new TextDecoder();
    const combined_string = Deno.UnsafePointerView.getCString(combined_ptr);
    console.log("Combined string:", combined_string);
    
    // Free the memory allocated in Rust
    lib.symbols.free_string(combined_ptr);
    
    lib.close();
    • Deno.dlopen:这个函数用于加载动态链接库。第一个参数是动态链接库的路径,第二个参数是一个对象,描述了我们要调用的函数。
    • parameters:这个数组描述了函数的参数类型。
    • result:这个字符串描述了函数的返回值类型。
    • lib.symbols.add:这是我们调用的 Rust 函数。
    • Deno.UnsafePointerDeno.UnsafePointerView: 用于在Deno和Rust之间传递指针。
    • 字符串处理需要特别注意,因为涉及到内存管理。Rust分配的内存需要手动释放,否则会造成内存泄漏。
  2. 运行 Deno 代码:

    运行以下命令运行 Deno 代码:

    deno run --allow-read --allow-ffi deno_app.ts
    • --allow-read:这个 flag 允许 Deno 读取文件。我们需要读取动态链接库。
    • --allow-ffi:这个 flag 允许 Deno 使用 FFI。

    如果一切顺利,你应该能在控制台中看到以下输出:

    10 + 20 = 30
    Combined string: Hello, Deno!

第三部分:进阶技巧:更复杂的数据类型和错误处理

上面的例子只是一个简单的演示。在实际开发中,我们可能需要处理更复杂的数据类型,比如结构体、数组、以及错误处理。

  1. 结构体:

    假设我们有一个 Rust 结构体:

    #[repr(C)]
    pub struct Point {
        pub x: i32,
        pub y: i32,
    }
    
    #[no_mangle]
    pub extern "C" fn create_point(x: i32, y: i32) -> Point {
        Point { x, y }
    }
    
    #[no_mangle]
    pub extern "C" fn get_point_x(point: Point) -> i32 {
        point.x
    }
    • #[repr(C)]:这个属性告诉 Rust 编译器,使用 C 兼容的内存布局。这非常重要,因为 Deno 需要知道结构体的内存布局才能正确地传递数据。

    在 Deno 中,我们可以这样使用:

    const lib = Deno.dlopen(
      "./target/release/libdeno_rust_lib.dylib", // 根据你的平台修改文件名
      {
        create_point: { parameters: ["i32", "i32"], result: "i64" }, // i64 用于表示结构体
        get_point_x: { parameters: ["i64"], result: "i32" },
      },
    );
    
    const point = lib.symbols.create_point(10, 20);
    const x = lib.symbols.get_point_x(point);
    console.log("Point X:", x);

    注意,这里我们将 Point 结构体映射成了 i64 类型。这是因为 Deno FFI 目前不支持直接传递结构体,我们需要将结构体转换为一个整数,然后在 Rust 中再将整数转换回结构体。

  2. 数组:

    在 Rust 中,我们可以这样定义一个数组:

    #[no_mangle]
    pub extern "C" fn sum_array(ptr: *const i32, len: usize) -> i32 {
        unsafe {
            let slice = std::slice::from_raw_parts(ptr, len);
            slice.iter().sum()
        }
    }

    在 Deno 中,我们可以这样使用:

    const lib = Deno.dlopen(
      "./target/release/libdeno_rust_lib.dylib", // 根据你的平台修改文件名
      {
        sum_array: { parameters: ["pointer", "usize"], result: "i32" },
      },
    );
    
    const array = new Int32Array([1, 2, 3, 4, 5]);
    const array_ptr = Deno.UnsafePointer.of(array);
    const array_len = array.length;
    
    const sum = lib.symbols.sum_array(array_ptr, array_len);
    console.log("Sum of array:", sum);
    • Deno.UnsafePointer.of(array):这个函数用于获取数组的指针。
    • array.length:这个属性用于获取数组的长度。
    • usize:Rust 中的 usize 类型对应 JavaScript 中的 number 类型。
  3. 错误处理:

    在 Rust 中,我们可以使用 Result 类型来表示可能发生的错误。

    use std::ffi::CString;
    use std::os::raw::c_char;
    
    #[no_mangle]
    pub extern "C" fn divide(a: i32, b: i32) -> Result<i32, String> {
        if b == 0 {
            Err("Division by zero".to_string())
        } else {
            Ok(a / b)
        }
    }
    
    #[no_mangle]
    pub extern "C" fn divide_wrapper(a: i32, b: i32) -> *mut c_char {
        match divide(a, b) {
            Ok(result) => {
                let s = result.to_string();
                let c_string = CString::new(s).unwrap();
                c_string.into_raw()
            }
            Err(err) => {
                let c_string = CString::new(err).unwrap();
                c_string.into_raw()
            }
        }
    }

    在 Deno 中,我们需要一个wrapper函数来处理 Result 返回值。

    const lib = Deno.dlopen(
      "./target/release/libdeno_rust_lib.dylib", // 根据你的平台修改文件名
      {
        divide_wrapper: { parameters: ["i32", "i32"], result: "pointer" },
        free_string: { parameters: ["pointer"], result: "void" }
      },
    );
    
    const resultPtr = lib.symbols.divide_wrapper(10, 0) as Deno.UnsafePointer;
    const decoder = new TextDecoder();
    const result = Deno.UnsafePointerView.getCString(resultPtr);
    
    if(isNaN(Number(result))){
        console.error("Error:", result);
    }else{
        console.log("Result:", result);
    }
    
    lib.symbols.free_string(resultPtr);

    这里,我们将 Result 类型的返回值转换成一个 C 风格的字符串。如果结果是数字则代表成功,如果不是数字,则代表错误信息。

第四部分:注意事项和最佳实践

  • 内存管理: Rust 负责分配的内存,必须由 Rust 负责释放。否则会造成内存泄漏。
  • 数据类型: Deno FFI 和 Rust FFI 之间的数据类型必须匹配。否则会导致程序崩溃。
  • 安全: FFI 是一项强大的技术,但也存在安全风险。我们需要仔细检查所有的数据,确保没有安全漏洞。
  • 错误处理: 我们需要仔细处理所有可能发生的错误,避免程序崩溃。
  • 性能: FFI 调用是有开销的。我们需要尽量减少 FFI 调用的次数,提高程序的性能。

总结:Deno FFI + Rust,让你的应用飞起来!

Deno FFI 结合 Rust FFI,为我们提供了一种构建高性能原生模块的强大方法。通过 Rust 的安全性和高效性,我们可以轻松地解决 JavaScript 无法解决的性能瓶颈。只要掌握了正确的技术和最佳实践,我们就可以让我们的 Deno 应用飞起来!

彩蛋:一些可能有用的表格

数据类型 Rust 类型 Deno FFI 类型
整数 i32, i64, u32, u64 "i32", "i64"
浮点数 f32, f64 "f32", "f64"
指针 *const T, *mut T "pointer"
无类型 () "void"
字符串 (C风格) *const c_char "pointer"
平台 动态链接库后缀
Linux .so
macOS .dylib
Windows .dll

好了,今天的讲座就到这里。希望大家有所收获!如果有什么问题,欢迎随时提问。下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注