Rust/C++ 互操作¶
本文档介绍如何在 Firefox 中使用 FFI 使 Rust 代码和 C++ 代码能够相互操作。
可传输类型¶
一般来说,要传输的数据越复杂,跨 FFI 边界的传输就越困难。
布尔值、整数和指针很少引起问题。
C++
bool
对应 Rustbool
C++
uint8_t
对应 Rustu8
,int32_t
对应 Rusti32
等。C++
const T*
对应 Rust*const T
,T*
对应 Rust*mut T
。
列表由 C++ nsTArray
和 Rust ThinVec
处理。
对于字符串,最好使用 nsstring
辅助库。使用原始指针加长度也可以用于字符串,但更容易出错。
如果需要哈希表,可能需要将其分解成两个列表(键和值)并分别传输。
其他类型可以使用生成绑定的工具进行处理,如下文所述。
堆分配¶
Firefox 中的 C++ 和 Rust 代码都使用相同的堆分配器,因此 C++ malloc
、free
及其相关函数可以与 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 的接口的详细说明,请参阅这篇博文。