Rust/C++ 互操作

本文档介绍如何在 Firefox 中使用 FFI 使 Rust 代码和 C++ 代码能够相互操作。

可传输类型

一般来说,要传输的数据越复杂,跨 FFI 边界的传输就越困难。

布尔值、整数和指针很少引起问题。

  • C++ bool 对应 Rust bool

  • C++ uint8_t 对应 Rust u8int32_t 对应 Rust i32 等。

  • C++ const T* 对应 Rust *const TT* 对应 Rust *mut T

列表由 C++ nsTArray 和 Rust ThinVec 处理。

对于字符串,最好使用 nsstring 辅助库。使用原始指针加长度也可以用于字符串,但更容易出错。

如果需要哈希表,可能需要将其分解成两个列表(键和值)并分别传输。

其他类型可以使用生成绑定的工具进行处理,如下文所述。

堆分配

Firefox 中的 C++ 和 Rust 代码都使用相同的堆分配器,因此 C++ mallocfree 及其相关函数可以与 Rust 的 std::alloc 函数互操作:由 Rust 在堆上分配的原始内存可以由 C++ 释放,反之亦然。例如,由 Rust Vec 分配的内存可以通过将其传递给 std::free 在 C++ 中释放。这是通过 mozglue-static 库中使用 Rust #[global_allocator] 属性的全局变量来实现的。

从 Rust 访问 C++ 代码和数据

要从 Rust 调用 C++ 函数,需要在 Rust 中添加函数声明。例如,对于此 C++ 函数

extern "C" {
bool UniquelyNamedFunction(const nsCString* aInput, nsCString* aRetVal) {
  return true;
}
}

将此声明添加到 Rust 代码中

extern "C" {
    pub fn UniquelyNamedFunction(input: &nsCString, ret_val: &mut nsCString) -> bool;
}

Rust 代码现在可以在 unsafe 块中调用 UniquelyNamedFunction()。请注意,如果声明不匹配(例如,由于 C++ 函数签名更改而没有更新 Rust 声明),则可能会导致崩溃。(因此需要 unsafe 块。)

由于这种不安全性,对于非平凡的接口(特别是当必须从 Rust 代码访问 C++ 结构体和类时),通常使用 rust-bindgen,它会生成 Rust 绑定。文档在这里

从 C++ 访问 Rust 代码和数据

从 C++ 访问 Rust 代码和数据的常用方法是使用 cbindgen,它会为公开 C API 的 Rust 库生成 C++ 头文件。cbindgen 是一个非常强大的工具,本节仅介绍其一些基本用法。

基础知识

首先,在 Rust 中添加合适的定义。#[no_mangle]extern "C" 是必需的。

#[no_mangle]
pub unsafe extern "C" fn unic_langid_canonicalize(
    langid: &nsCString,
    ret_val: &mut nsCString
) -> bool {
    ret_val.assign("new value");
    true
}

然后,在库的根目录中添加一个 cbindgen.toml 文件。它可能如下所示

header = """/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */"""
autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */
#ifndef mozilla_intl_locale_MozLocaleBindings_h
#error "Don't include this file directly, instead include MozLocaleBindings.h"
#endif
"""
include_version = true
braces = "SameLine"
line_length = 100
tab_width = 2
language = "C++"
# Put FFI calls in the `mozilla::intl::ffi` namespace.
namespaces = ["mozilla", "intl", "ffi"]

# Export `ThinVec` references as `nsTArray`.
[export.rename]
"ThinVec" = "nsTArray"

接下来,扩展相关的 moz.build 文件以调用 cbindgen。

if CONFIG['COMPILE_ENVIRONMENT']:
    CbindgenHeader('unic_langid_ffi_generated.h',
                   inputs=['/intl/locale/rust/unic-langid-ffi'])

    EXPORTS.mozilla.intl += [
        '!unic_langid_ffi_generated.h',
    ]

这告诉构建系统在 intl/locale/rust/unic-langid-ffi 上运行 cbindgen 以生成 unic_langid_ffi_generated.h,该文件将放置在 $OBJDIR/dist/include/mozilla/intl/ 中。

最后,将生成的头部包含到 C++ 文件中并调用该函数。

#include "mozilla/intl/unic_langid_ffi_generated.h"

using namespace mozilla::intl::ffi;

void Locale::MyFunction(nsCString& aInput) const {
  nsCString result;
  unic_langid_canonicalize(aInput, &result);
}

复杂类型

许多复杂的 Rust 类型可以公开给 C++,并且 cbindgen 将为所有 pub 类型生成相应的绑定。例如

#[repr(C)]
pub enum FluentPlatform {
    Linux,
    Windows,
    Macos,
    Android,
    Other,
}

extern "C" {
    pub fn FluentBuiltInGetPlatform() -> FluentPlatform;
}
ffi::FluentPlatform FluentBuiltInGetPlatform() {
  return ffi::FluentPlatform::Linux;
}

有关使用 cbindgen 将更复杂的 Rust 类型公开给 C++ 的示例,请参阅这篇博文

实例

如果需要从 C++ 代码创建和销毁 Rust 结构体,以下示例可能会有所帮助。

首先,在 Rust 中定义构造函数、析构函数和 getter 函数。(这些函数的 C++ 声明将由 cbindgen 生成。)

#[no_mangle]
pub unsafe extern "C" fn unic_langid_new() -> *mut LanguageIdentifier {
    let langid = LanguageIdentifier::default();
    Box::into_raw(Box::new(langid))
}

#[no_mangle]
pub unsafe extern "C" fn unic_langid_destroy(langid: *mut LanguageIdentifier) {
    drop(Box::from_raw(langid));
}

#[no_mangle]
pub unsafe extern "C" fn unic_langid_as_string(
    langid: &mut LanguageIdentifier,
    ret_val: &mut nsACString,
) {
    ret_val.assign(&langid.to_string());
}

接下来,在 C++ 头文件中通过 DefaultDelete 定义一个析构函数。

#include "mozilla/intl/unic_langid_ffi_generated.h"
#include "mozilla/UniquePtr.h"

namespace mozilla {

template <>
class DefaultDelete<intl::ffi::LanguageIdentifier> {
 public:
  void operator()(intl::ffi::LanguageIdentifier* aPtr) const {
    unic_langid_destroy(aPtr);
  }
};

}  // namespace mozilla

(此定义必须在任何使用 UniquePtr<intl::ffi::LanguageIdentifier> 的地方可见,否则 C++ 将尝试释放代码,这可能会导致奇怪的行为!)

最后,实现类。

class Locale {
public:
  explicit Locale(const nsACString& aLocale)
    : mRaw(unic_langid_new()) {}

  const nsCString Locale::AsString() const {
    nsCString tag;
    unic_langid_as_string(mRaw.get(), &tag);
    return tag;
  }

private:
  UniquePtr<ffi::LanguageIdentifier> mRaw;
}

这使得能够实例化一个 Locale 对象并调用 AsString(),所有这些都来自 C++ 代码。

其他示例

有关 Firefox 中不使用 cbindgen 或 rust-bindgen 的接口的详细说明,请参阅这篇博文