引言:Dart FFI 与 C++ 互操作的挑战
在现代软件开发中,不同编程语言间的互操作性变得日益重要。Dart 作为 Google 力推的客户端优先语言,凭借其高效的 UI 渲染能力和跨平台特性,在移动和桌面应用开发中占据一席之地。然而,许多高性能、低延迟或依赖特定硬件的复杂逻辑往往已经用 C、C++ 等原生语言实现。为了利用这些既有资产,Dart 提供了外部函数接口(Foreign Function Interface,FFI),允许 Dart 代码直接调用 C 语言风格的函数,并与原生数据结构进行交互。
Dart FFI 的强大之处在于它能够桥接 Dart 虚拟机(VM)与原生代码之间的鸿沟。它允许我们加载动态库(如 .so、.dll、.dylib),查找并调用其中的 C 函数。这为 Dart 应用带来了无与伦比的扩展性,例如集成操作系统 API、使用高性能计算库、访问硬件设备驱动等。
然而,当涉及 C++ 对象时,事情变得复杂。C++ 是一门面向对象的语言,其核心特点之一是强大的资源管理能力,尤其是通过构造函数和析构函数对对象的生命周期进行精确控制。Dart VM 则有自己的垃圾回收(Garbage Collection, GC)机制来管理 Dart 对象的内存。在 FFI 场景下,我们通常会在 C++ 侧创建一个对象,然后将其内存地址(指针)传递给 Dart。此时,Dart VM 并不知晓这个指针指向的 C++ 对象的内部结构,更无法自动管理其生命周期。这就造成了一个核心挑战:如何确保当 Dart 侧不再需要这个 C++ 对象时,其在 C++ 侧占用的资源能够被正确、及时地释放,从而避免内存泄漏、资源泄露,甚至程序崩溃。
C++ 对象生命周期管理之痛:手动 new/delete 的陷阱
在没有自动化机制的情况下,管理 Dart FFI 中 C++ 对象的生命周期通常意味着在 Dart 代码中手动调用一个 C 函数,该函数负责在 C++ 侧 delete 相应的对象。例如,如果我们在 C++ 库中有一个 MyObject 类,通常会导出类似 createMyObject() 和 destroyMyObject(MyObject* obj) 的 C 风格函数。
Dart 代码可能会这样使用:
// Dart code (simplified)
Pointer<Void> myObjectPointer = createMyObject();
// Use myObjectPointer...
destroyMyObject(myObjectPointer); // <--- 手动释放
这种手动管理模式带来了显著的问题:
- 忘记释放 (Memory Leaks):开发者很容易忘记调用
destroyMyObject,尤其是在复杂的逻辑分支、异常处理或应用程序退出时。这将导致 C++ 资源得不到释放,造成内存泄漏或其他资源泄露。 - 过早释放 (Use-After-Free):如果
destroyMyObject被过早调用,而 Dart 代码仍然持有并尝试访问该指针,就会导致使用已释放内存的错误(Use-After-Free),这通常会导致程序崩溃或不可预测的行为。 - 重复释放 (Double Free):如果
destroyMyObject被多次调用,试图释放同一块内存两次,这也可能导致程序崩溃或内存损坏。 - 非确定性:GC 的非确定性使得我们无法预知 Dart 对象何时会被回收。如果 C++ 对象的生命周期完全依赖于 Dart 对象的 GC,而我们又没有一个自动机制来触发 C++ 析构,那么 C++ 对象就可能永远不会被清理。
- 代码冗余与维护困难:每个 C++ 对象的创建和销毁都需要配套的手动调用,使得 Dart 代码变得臃肿且容易出错。
这些问题在 C++ 编程中同样存在,这也是 C++ 社区引入 RAII (Resource Acquisition Is Initialization) 模式和智能指针等机制的根本原因。
C++ 的 RAII 哲学:资源管理的核心
RAII,即“资源获取即初始化”,是 C++ 中一种核心的资源管理技术和编程范式。它的基本思想是:将资源的生命周期绑定到对象的生命周期。当对象被创建时(通过构造函数),它获取资源;当对象被销毁时(通过析构函数),它释放资源。
RAII 的核心原则:
- 资源与对象绑定:将资源的获取(如打开文件、分配内存、获取锁)放在类的构造函数中。
- 资源自动释放:将资源的释放放在类的析构函数中。
- 栈语义:当对象超出其作用域时(无论是正常退出、函数返回还是异常抛出),其析构函数都会被自动调用,从而确保资源得到释放。
例如,C++ 中的 std::unique_ptr 和 std::shared_ptr 就是 RAII 模式的典型应用。它们封装了原始指针,并在其析构函数中自动调用 delete,从而避免了手动管理内存的繁琐和错误。
// C++ RAII 示例:使用 std::unique_ptr 管理动态内存
#include <iostream>
#include <memory> // For std::unique_ptr
class MyResource {
public:
MyResource(int id) : id_(id) {
std::cout << "MyResource " << id_ << " acquired." << std::endl;
}
~MyResource() {
std::cout << "MyResource " << id_ << " released." << std::endl;
}
void doSomething() {
std::cout << "MyResource " << id_ << " doing something." << std::endl;
}
private:
int id_;
};
void functionThatUsesResource() {
// 资源在构造函数中获取
std::unique_ptr<MyResource> res = std::make_unique<MyResource>(123);
res->doSomething();
// 当 res 超出作用域时,MyResource 的析构函数会被自动调用,释放资源
} // res 在这里被销毁
int main() {
functionThatUsesResource();
std::cout << "Function finished." << std::endl;
return 0;
}
在 C++ 内部,RAII 机制非常强大,它使得资源管理变得自动化、安全且异常安全。然而,当我们将 C++ 对象指针传递给 Dart 时,Dart VM 无法感知 C++ 对象的 RAII 语义。Dart 仅仅持有一个原始指针,当 Dart 侧的包装对象被 GC 回收时,C++ 对象的析构函数并不会被自动调用。我们需要一种机制,将 Dart 对象的生命周期与 C++ 对象的析构函数调用联系起来,将 C++ 的 RAII 理念延伸到 FFI 边界之外。
Dart 的解决方案:NativeFinalizer 机制
为了解决 Dart FFI 中 C++ 对象生命周期管理的挑战,Dart 引入了 NativeFinalizer。NativeFinalizer 是 Dart VM 提供的一种机制,允许开发者注册一个回调函数(通常是 C 函数),当一个特定的 Dart 对象被垃圾回收器回收时,VM 会在一个独立的终结器(Finalizer)线程上调用这个注册的 C 函数。这个 C 函数通常就是 C++ 对象的析构函数或一个封装了析构逻辑的 C 风格函数。
NativeFinalizer 的核心思想是:将一个 Dart 对象的生命周期与一个原生资源(如 C++ 对象)的释放逻辑关联起来。当 Dart 对象变得不可达并被 GC 回收时,NativeFinalizer 会自动触发对原生资源的清理。
NativeFinalizer 的工作原理:
- 注册终结器:创建一个
NativeFinalizer实例,并提供一个指向 C 函数的指针。这个 C 函数就是当 Dart 对象被回收时需要执行的清理逻辑。 - 关联对象:使用
NativeFinalizer.attach()方法将终结器关联到一个 Dart 对象(managedObject)和一个原生资源的地址(externalAddress,通常是 C++ 对象的指针)。 - GC 触发:当
managedObject变得不可达,并被 Dart VM 的垃圾回收器检测到即将被回收时,VM 会将externalAddress连同终结器信息发送到一个独立的终结器线程。 - 执行清理:终结器线程会调用之前注册的 C 函数,并将
externalAddress作为参数传递给它。这个 C 函数随后负责释放externalAddress所指向的原生资源。
NativeFinalizer 的特性和局限性:
- 非确定性:与所有垃圾回收机制一样,
NativeFinalizer的触发是非确定性的。我们无法精确预测 Dart 对象何时会被 GC 回收,因此也无法精确预测 C++ 对象何时会被释放。它可能在对象变得不可达后立即发生,也可能在一段时间后才发生。 - 不保证执行:在 Dart 应用程序完全关闭时,VM 可能不会执行所有待处理的终结器。这意味着,如果一个 C++ 对象只依赖
NativeFinalizer进行清理,并且应用程序突然退出,那么该对象可能不会被正常析构,可能导致资源泄露(例如文件句柄、网络连接等非内存资源)。 - 独立线程:终结器回调在一个独立的 Dart VM 内部线程上执行。这意味着回调函数必须是线程安全的,并且不能访问任何可能已经被回收的 Dart 对象。
- 只适用于原生资源:
NativeFinalizer专门用于清理原生资源,而不是 Dart 内存。Dart 内存由 GC 自动管理。 - 单参数回调:注册的 C 函数只能接受一个
Pointer<Void>类型的参数,即externalAddress。这意味着如果需要传递更多信息,需要将它们封装到externalAddress所指向的结构中。
尽管有非确定性和不保证执行的局限性,NativeFinalizer 仍然是 Dart FFI 中实现 C++ 对象 RAII 模式的强大工具。它极大地简化了内存管理,并避免了大多数常见的内存泄漏问题,是连接 Dart GC 和 C++ 析构逻辑的关键桥梁。通常,为了应对非确定性问题和应用程序关闭时的资源释放,我们还会为 Dart 包装类提供一个显式的 dispose() 方法,允许开发者在需要时立即释放 C++ 资源。
构建 FFI 友好的 C++ 接口
要在 Dart 中利用 NativeFinalizer 管理 C++ 对象,我们首先需要在 C++ 侧提供一个符合 FFI 规范的接口。这意味着我们需要将 C++ 类封装成 C 风格的函数,以便 Dart FFI 能够直接调用。
核心思想:
- C++ 类:定义你的 C++ 类,包含构造函数、析构函数和成员方法。
- C 风格包装函数:
- 工厂函数:用于创建 C++ 类的实例,并返回其指针。
- 成员方法包装函数:接受 C++ 对象指针作为第一个参数,然后是原 C++ 方法的参数。
- 析构函数:接受 C++ 对象指针作为参数,负责
delete该对象。这是NativeFinalizer将调用的函数。
我们将创建一个简单的 MyNativeClass 作为示例。
示例 C++ 类:MyNativeClass
这个类将有一个构造函数、一个析构函数和一个简单的成员方法。
my_native_class.h
#ifndef MY_NATIVE_CLASS_H
#define MY_NATIVE_CLASS_H
#include <string>
#include <iostream>
// C++ class definition
class MyNativeClass {
public:
MyNativeClass(int id, const std::string& name);
~MyNativeClass();
void greet() const;
int getId() const;
std::string getName() const;
private:
int id_;
std::string name_;
};
// C-style interface for Dart FFI
extern "C" { // Ensure C linkage for FFI compatibility
// Function to create an instance of MyNativeClass
// Returns a pointer to the created object (void* for FFI)
MyNativeClass* create_my_native_class(int id, const char* name_ptr);
// Function to call the greet method
void my_native_class_greet(MyNativeClass* obj);
// Function to get the ID
int my_native_class_get_id(MyNativeClass* obj);
// Function to get the name (returns a C-string, caller must not free)
const char* my_native_class_get_name(MyNativeClass* obj);
// Function to destroy an instance of MyNativeClass
// This is the function that NativeFinalizer will call
void destroy_my_native_class(MyNativeClass* obj);
} // extern "C"
#endif // MY_NATIVE_CLASS_H
my_native_class.cpp
#include "my_native_class.h"
#include <cstring> // For strlen, strcpy if needed (not directly here)
// C++ class implementation
MyNativeClass::MyNativeClass(int id, const std::string& name)
: id_(id), name_(name) {
std::cout << "[C++] MyNativeClass " << id_ << " ('" << name_ << "') constructed." << std::endl;
}
MyNativeClass::~MyNativeClass() {
std::cout << "[C++] MyNativeClass " << id_ << " ('" << name_ << "') destructed." << std::endl;
}
void MyNativeClass::greet() const {
std::cout << "[C++] Hello from MyNativeClass " << id_ << ", my name is " << name_ << "." << std::endl;
}
int MyNativeClass::getId() const {
return id_;
}
std::string MyNativeClass::getName() const {
return name_;
}
// C-style interface implementation
extern "C" {
MyNativeClass* create_my_native_class(int id, const char* name_ptr) {
// We expect name_ptr to be a valid C-string from Dart
return new MyNativeClass(id, std::string(name_ptr));
}
void my_native_class_greet(MyNativeClass* obj) {
if (obj) {
obj->greet();
} else {
std::cerr << "[C++] Error: my_native_class_greet called with null object." << std::endl;
}
}
int my_native_class_get_id(MyNativeClass* obj) {
if (obj) {
return obj->getId();
} else {
std::cerr << "[C++] Error: my_native_class_get_id called with null object." << std::endl;
return -1; // Or throw an error, depending on error handling strategy
}
}
const char* my_native_class_get_name(MyNativeClass* obj) {
if (obj) {
// IMPORTANT: The string returned here is owned by the C++ object.
// It must NOT be freed by Dart. Its lifetime is tied to the C++ object.
// If Dart needs to own it, a copy must be made (e.g., using strdup)
// and Dart must be responsible for freeing that copy.
// For simplicity, we return a reference to the internal string's C-string.
return obj->getName().c_str();
} else {
std::cerr << "[C++] Error: my_native_class_get_name called with null object." << std::endl;
return nullptr;
}
}
void destroy_my_native_class(MyNativeClass* obj) {
if (obj) {
delete obj;
} else {
std::cerr << "[C++] Warning: destroy_my_native_class called with null object." << std::endl;
}
}
} // extern "C"
CMakeLists.txt (用于构建共享库)
为了构建这个 C++ 代码为 Dart 可以加载的共享库,我们需要一个 CMakeLists.txt 文件。
cmake_minimum_required(VERSION 3.10)
project(my_native_library CXX)
# Set common C++ standards
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Add source files
set(SOURCES
my_native_class.cpp
)
# Create a shared library
# On Linux: libmy_native_library.so
# On macOS: libmy_native_library.dylib
# On Windows: my_native_library.dll
add_library(my_native_library SHARED ${SOURCES})
# Specify output directory (optional, but good for organization)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
# Example usage to build:
# mkdir build
# cd build
# cmake ..
# cmake --build .
# The shared library will be in build/lib/
构建库的步骤:
- 保存
my_native_class.h,my_native_class.cpp,CMakeLists.txt到同一个目录下。 - 打开终端,进入该目录。
mkdir buildcd buildcmake ..cmake --build .
成功构建后,你会在 build/lib 目录下找到 libmy_native_library.so (Linux), libmy_native_library.dylib (macOS) 或 my_native_library.dll (Windows)。
在 Dart 中封装 Native 对象
有了 C++ 共享库和 C 风格接口,我们现在可以在 Dart 中创建包装器,利用 NativeFinalizer 自动管理 C++ 对象的生命周期。
1. 加载动态库
首先,我们需要加载之前构建的共享库。库的名称会因操作系统而异。
import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:ffi/ffi.dart'; // For .toNativeUtf8(), .toDartString()
// Determine the correct library name based on the platform
final String _libraryPath = Platform.isMacOS
? 'build/lib/libmy_native_library.dylib'
: Platform.isLinux
? 'build/lib/libmy_native_library.so'
: Platform.isWindows
? 'build/lib/my_native_library.dll'
: throw Exception('Unsupported platform');
// Load the dynamic library
final DynamicLibrary myNativeLib = DynamicLibrary.open(_libraryPath);
2. 定义 FFI 函数签名
接下来,我们需要为 C++ 导出的 C 风格函数定义 Dart FFI 签名。这包括 C 函数的类型签名(NativeFunction)和 Dart 中可调用的函数类型签名(DartFunction)。
// C-style function definitions from my_native_class.h
// MyNativeClass* create_my_native_class(int id, const char* name_ptr);
typedef Pointer<Void> CreateMyNativeClass_Native(Int32 id, Pointer<Utf8> name_ptr);
typedef Pointer<Void> CreateMyNativeClass_Dart(int id, Pointer<Utf8> name_ptr);
// void my_native_class_greet(MyNativeClass* obj);
typedef Void MyNativeClassGreet_Native(Pointer<Void> obj);
typedef void MyNativeClassGreet_Dart(Pointer<Void> obj);
// int my_native_class_get_id(MyNativeClass* obj);
typedef Int32 MyNativeClassGetId_Native(Pointer<Void> obj);
typedef int MyNativeClassGetId_Dart(Pointer<Void> obj);
// const char* my_native_class_get_name(MyNativeClass* obj);
typedef Pointer<Utf8> MyNativeClassGetName_Native(Pointer<Void> obj);
typedef Pointer<Utf8> MyNativeClassGetName_Dart(Pointer<Void> obj);
// void destroy_my_native_class(MyNativeClass* obj);
// This is the function that NativeFinalizer will call.
typedef Void DestroyMyNativeClass_Native(Pointer<Void> obj);
typedef void DestroyMyNativeClass_Dart(Pointer<Void> obj);
3. 查找 FFI 函数
使用 DynamicLibrary.lookupFunction 将原生函数绑定到 Dart 函数。
// Lookup the functions
final CreateMyNativeClass_Dart _createMyNativeClass = myNativeLib
.lookupFunction<CreateMyNativeClass_Native, CreateMyNativeClass_Dart>(
'create_my_native_class');
final MyNativeClassGreet_Dart _myNativeClassGreet = myNativeLib
.lookupFunction<MyNativeClassGreet_Native, MyNativeClassGreet_Dart>(
'my_native_class_greet');
final MyNativeClassGetId_Dart _myNativeClassGetId = myNativeLib
.lookupFunction<MyNativeClassGetId_Native, MyNativeClassGetId_Dart>(
'my_native_class_get_id');
final MyNativeClassGetName_Dart _myNativeClassGetName = myNativeLib
.lookupFunction<MyNativeClassGetName_Native, MyNativeClassGetName_Dart>(
'my_native_class_get_name');
// The destructor function pointer is crucial for NativeFinalizer
final Pointer<NativeFinalizerFunction> _destroyMyNativeClassPointer = myNativeLib
.lookup<NativeFinalizerFunction>('destroy_my_native_class');
这里 _destroyMyNativeClassPointer 是一个 Pointer<NativeFinalizerFunction> 类型,它直接指向 C 函数 destroy_my_native_class。这个指针将作为参数传递给 NativeFinalizer 构造函数。
4. 创建 Dart 包装类 (MyNativeObject)
现在,我们将创建一个 Dart 类 MyNativeObject 来封装 C++ MyNativeClass 的实例。这个类将持有 C++ 对象的指针,并集成 NativeFinalizer。
class MyNativeObject {
// The raw pointer to the C++ MyNativeClass instance
// It's marked as late final because it's initialized in the constructor,
// but it ensures that once set, it won't change.
// We use Pointer<Void> as the generic type for C++ objects.
late final Pointer<Void> _nativeInstancePointer;
// The NativeFinalizer instance.
// It's static because a single finalizer instance can be attached to multiple Dart objects.
static final NativeFinalizer _finalizer =
NativeFinalizer(_destroyMyNativeClassPointer);
// Constructor: Creates a C++ object and attaches the finalizer.
MyNativeObject(int id, String name) {
// Allocate C-string for name
final Pointer<Utf8> namePtr = name.toNativeUtf8();
// Create the C++ object
_nativeInstancePointer = _createMyNativeClass(id, namePtr);
// Free the C-string allocated for name
malloc.free(namePtr);
// Attach the finalizer:
// When *this* Dart object (MyNativeObject instance) is GC'd,
// call _destroyMyNativeClassPointer with _nativeInstancePointer as argument.
_finalizer.attach(this, _nativeInstancePointer, token: _nativeInstancePointer);
print('[Dart] MyNativeObject($id, "$name") created and finalizer attached.');
}
// Member methods wrapping C++ calls
void greet() {
_myNativeClassGreet(_nativeInstancePointer);
}
int getId() {
return _myNativeClassGetId(_nativeInstancePointer);
}
String getName() {
final Pointer<Utf8> namePtr = _myNativeClassGetName(_nativeInstancePointer);
if (namePtr == nullptr) {
return 'Error: Native object name is null.';
}
// IMPORTANT: The C++ side returns a pointer to an internal string.
// Dart creates a copy here. The original C++ string is owned by the C++ object.
return namePtr.toDartString();
}
// Explicit dispose method for immediate cleanup
// This is a best practice for deterministic resource release.
void dispose() {
if (_nativeInstancePointer != nullptr) {
print('[Dart] MyNativeObject(${getId()}, "${getName()}") explicitly disposing.');
// Detach the finalizer to prevent double-free when GC happens later.
_finalizer.detach(this);
// Manually call the C++ destructor
_destroyMyNativeClassPointer.asFunction<DestroyMyNativeClass_Dart>()(_nativeInstancePointer);
// Invalidate the pointer to prevent further use and double-free
_nativeInstancePointer = nullptr;
} else {
print('[Dart] Attempted to dispose already disposed MyNativeObject.');
}
}
}
5. 完整 Dart 示例 (main.dart)
import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:ffi/ffi.dart'; // For .toNativeUtf8(), .toDartString(), malloc
// --- FFI Setup (as defined above) ---
// Determine the correct library name based on the platform
final String _libraryPath = Platform.isMacOS
? 'build/lib/libmy_native_library.dylib'
: Platform.isLinux
? 'build/lib/libmy_native_library.so'
: Platform.isWindows
? 'build/lib/my_native_library.dll'
: throw Exception('Unsupported platform');
// Load the dynamic library
final DynamicLibrary myNativeLib = DynamicLibrary.open(_libraryPath);
// C-style function definitions from my_native_class.h
typedef Pointer<Void> CreateMyNativeClass_Native(Int32 id, Pointer<Utf8> name_ptr);
typedef Pointer<Void> CreateMyNativeClass_Dart(int id, Pointer<Utf8> name_ptr);
typedef Void MyNativeClassGreet_Native(Pointer<Void> obj);
typedef void MyNativeClassGreet_Dart(Pointer<Void> obj);
typedef Int32 MyNativeClassGetId_Native(Pointer<Void> obj);
typedef int MyNativeClassGetId_Dart(Pointer<Void> obj);
typedef Pointer<Utf8> MyNativeClassGetName_Native(Pointer<Void> obj);
typedef Pointer<Utf8> MyNativeClassGetName_Dart(Pointer<Void> obj);
typedef Void DestroyMyNativeClass_Native(Pointer<Void> obj);
typedef void DestroyMyNativeClass_Dart(Pointer<Void> obj);
// Lookup the functions
final CreateMyNativeClass_Dart _createMyNativeClass = myNativeLib
.lookupFunction<CreateMyNativeClass_Native, CreateMyNativeClass_Dart>(
'create_my_native_class');
final MyNativeClassGreet_Dart _myNativeClassGreet = myNativeLib
.lookupFunction<MyNativeClassGreet_Native, MyNativeClassGreet_Dart>(
'my_native_class_greet');
final MyNativeClassGetId_Dart _myNativeClassGetId = myNativeLib
.lookupFunction<MyNativeClassGetId_Native, MyNativeClassGetId_Dart>(
'my_native_class_get_id');
final MyNativeClassGetName_Dart _myNativeClassGetName = myNativeLib
.lookupFunction<MyNativeClassGetName_Native, MyNativeClassGetName_Dart>(
'my_native_class_get_name');
final Pointer<NativeFinalizerFunction> _destroyMyNativeClassPointer = myNativeLib
.lookup<NativeFinalizerFunction>('destroy_my_native_class');
// --- MyNativeObject class (as defined above) ---
class MyNativeObject {
late Pointer<Void> _nativeInstancePointer;
static final NativeFinalizer _finalizer =
NativeFinalizer(_destroyMyNativeClassPointer);
MyNativeObject(int id, String name) {
final Pointer<Utf8> namePtr = name.toNativeUtf8();
_nativeInstancePointer = _createMyNativeClass(id, namePtr);
malloc.free(namePtr);
_finalizer.attach(this, _nativeInstancePointer, token: _nativeInstancePointer);
print('[Dart] MyNativeObject($id, "$name") created and finalizer attached.');
}
void greet() {
if (_nativeInstancePointer == nullptr) {
print('[Dart] Error: Cannot call greet on a disposed object.');
return;
}
_myNativeClassGreet(_nativeInstancePointer);
}
int getId() {
if (_nativeInstancePointer == nullptr) {
print('[Dart] Error: Cannot get ID from a disposed object, returning -1.');
return -1;
}
return _myNativeClassGetId(_nativeInstancePointer);
}
String getName() {
if (_nativeInstancePointer == nullptr) {
print('[Dart] Error: Cannot get name from a disposed object, returning empty string.');
return '';
}
final Pointer<Utf8> namePtr = _myNativeClassGetName(_nativeInstancePointer);
if (namePtr == nullptr) {
return 'Error: Native object name is null.';
}
return namePtr.toDartString();
}
void dispose() {
if (_nativeInstancePointer != nullptr) {
print('[Dart] MyNativeObject(${getId()}, "${getName()}") explicitly disposing.');
_finalizer.detach(this); // Detach to prevent double-free
_destroyMyNativeClassPointer.asFunction<DestroyMyNativeClass_Dart>()(_nativeInstancePointer);
_nativeInstancePointer = nullptr; // Invalidate pointer
print('[Dart] MyNativeObject explicitly disposed.');
} else {
print('[Dart] Attempted to dispose already disposed MyNativeObject (no-op).');
}
}
}
// --- Main application logic ---
void main() async {
print('--- Starting Dart FFI RAII Example ---');
// Scenario 1: Object cleaned up by NativeFinalizer (implicit)
print('nScenario 1: Implicit cleanup by NativeFinalizer');
{
MyNativeObject obj1 = MyNativeObject(101, 'Alice');
obj1.greet();
print('ID: ${obj1.getId()}, Name: ${obj1.getName()}');
// obj1 goes out of scope here. Dart GC will eventually collect it.
} // obj1 becomes eligible for GC
// Give GC a chance to run (not guaranteed, but often helps for demonstration)
// In a real app, you wouldn't typically call this.
await Future.delayed(Duration(milliseconds: 100));
print('Dart is trying to collect garbage for obj1...');
// This is a hack for demonstration, not for production use.
// The actual GC timing is non-deterministic.
// We cannot directly force GC in Dart. This is just a pause.
// Forcing a lot of allocations can sometimes trigger it.
// We'll create some dummy objects to increase memory pressure.
for (int i = 0; i < 100000; i++) {
List<int> dummy = List.filled(1000, i);
}
await Future.delayed(Duration(milliseconds: 100)); // Give finalizer thread time
// Scenario 2: Object cleaned up explicitly by dispose()
print('nScenario 2: Explicit cleanup by dispose()');
MyNativeObject obj2 = MyNativeObject(202, 'Bob');
obj2.greet();
print('ID: ${obj2.getId()}, Name: ${obj2.getName()}');
obj2.dispose(); // Explicitly clean up C++ resource
obj2.greet(); // Attempt to use after dispose - should show error
// Scenario 3: Multiple objects, mixed cleanup
print('nScenario 3: Multiple objects, mixed cleanup');
MyNativeObject obj3 = MyNativeObject(303, 'Charlie');
MyNativeObject obj4 = MyNativeObject(404, 'David');
obj3.greet();
obj4.greet();
obj3.dispose(); // obj3 explicitly cleaned
// obj4 will be implicitly cleaned by finalizer later
// Ensure main isolate doesn't exit before finalizers run (for demonstration)
// In a real app, this might not be necessary, but for showing finalizer output, it helps.
print('nWaiting for potential finalizer calls for remaining objects...');
await Future.delayed(Duration(seconds: 2)); // Give finalizer thread more time
for (int i = 0; i < 100000; i++) {
List<int> dummy = List.filled(1000, i);
}
await Future.delayed(Duration(seconds: 1)); // Give finalizer thread more time
print('--- Dart FFI RAII Example Finished ---');
}
运行 Dart 应用程序:
dart run main.dart
预期输出(可能顺序略有不同,特别是 NativeFinalizer 触发的时间):
--- Starting Dart FFI RAII Example ---
Scenario 1: Implicit cleanup by NativeFinalizer
[C++] MyNativeClass 101 ('Alice') constructed.
[Dart] MyNativeObject(101, "Alice") created and finalizer attached.
[C++] Hello from MyNativeClass 101, my name is Alice.
ID: 101, Name: Alice
Dart is trying to collect garbage for obj1...
[C++] MyNativeClass 101 ('Alice') destructed. <-- NativeFinalizer for obj1 triggered here (eventually)
Scenario 2: Explicit cleanup by dispose()
[C++] MyNativeClass 202 ('Bob') constructed.
[Dart] MyNativeObject(202, "Bob") created and finalizer attached.
[C++] Hello from MyNativeClass 202, my name is Bob.
ID: 202, Name: Bob
[Dart] MyNativeObject(202, "Bob") explicitly disposing.
[Dart] MyNativeObject explicitly disposed.
[C++] MyNativeClass 202 ('Bob') destructed. <-- Explicit dispose for obj2
[Dart] Error: Cannot call greet on a disposed object.
Scenario 3: Multiple objects, mixed cleanup
[C++] MyNativeClass 303 ('Charlie') constructed.
[Dart] MyNativeObject(303, "Charlie") created and finalizer attached.
[C++] MyNativeClass 404 ('David') constructed.
[Dart] MyNativeObject(404, "David") created and finalizer attached.
[C++] Hello from MyNativeClass 303, my name is Charlie.
[C++] Hello from MyNativeClass 404, my name is David.
[Dart] MyNativeObject(303, "Charlie") explicitly disposing.
[Dart] MyNativeObject explicitly disposed.
[C++] MyNativeClass 303 ('Charlie') destructed. <-- Explicit dispose for obj3
Waiting for potential finalizer calls for remaining objects...
[C++] MyNativeClass 404 ('David') destructed. <-- NativeFinalizer for obj4 triggered here (eventually)
--- Dart FFI RAII Example Finished ---
这个例子清晰地展示了 NativeFinalizer 如何在 Dart 对象被 GC 后自动清理 C++ 资源,以及 dispose() 方法如何提供显式的、确定性的清理机制。
NativeFinalizer 深入解析
让我们更详细地了解 NativeFinalizer 的内部机制和其 attach 方法的参数。
NativeFinalizer 类位于 dart:ffi 库中,它的核心功能是注册一个回调,以便在 Dart 对象被回收时执行原生代码。
1. NativeFinalizer 构造函数
NativeFinalizer 的构造函数接受一个 Pointer<NativeFinalizerFunction> 参数,以及一个可选的 size 参数。
NativeFinalizer(Pointer<NativeFinalizerFunction> callback, [int size = 0])
callback(类型Pointer<NativeFinalizerFunction>):
这是一个指向 C 函数的指针。当NativeFinalizer被触发时,VM 会调用这个 C 函数。NativeFinalizerFunction是一个 typedef,其签名是void Function(Pointer<Void> data)。这意味着你的 C 回调函数必须接受一个void*参数,并且不返回任何值。这个void*参数就是你在attach方法中提供的externalAddress。size(类型int,默认0):
这是一个可选参数,用于向 Dart VM 提示被NativeFinalizer管理的原生资源大致占用的字节数。这个信息对 Dart GC 来说是一个提示,可以帮助 GC 更好地进行调度和内存管理决策,例如,如果一个对象的原生资源很大,GC 可能会更积极地回收它。它不会直接影响回调函数的行为,也不是 C++ 对象实际占用的内存大小,而是一个估算值。
在我们的例子中:
final Pointer<NativeFinalizerFunction> _destroyMyNativeClassPointer = myNativeLib
.lookup<NativeFinalizerFunction>('destroy_my_native_class');
static final NativeFinalizer _finalizer =
NativeFinalizer(_destroyMyNativeClassPointer);
这里 _destroyMyNativeClassPointer 确实符合 NativeFinalizerFunction 的签名要求,因为它指向的 C 函数 destroy_my_native_class 接受一个 MyNativeClass* (即 void*) 并返回 void。
2. attach 方法
NativeFinalizer 实例创建后,需要通过 attach 方法将其关联到特定的 Dart 对象和原生资源。
void attach(Object managedObject, Pointer<NativeType> externalAddress, {Object? token})
managedObject(类型Object):
这是 Dart VM 需要监控的 Dart 对象。当这个managedObject变得不可达并被垃圾回收时,NativeFinalizer就会被触发。在我们的例子中,这个是MyNativeObject的实例 (this)。externalAddress(类型Pointer<NativeType>):
这是原生资源的内存地址(指针)。当NativeFinalizer的回调函数被调用时,这个externalAddress会作为参数传递给它。在我们的例子中,这是_nativeInstancePointer,它指向 C++MyNativeClass的实例。token(类型Object?,可选):
这个参数是可选的,用于在同一个managedObject上附加多个NativeFinalizer时进行区分。通常情况下,我们为一个 Dart 对象只附加一个NativeFinalizer来清理其唯一的原生资源,此时token可以是null或与externalAddress相同。在我们的例子中,我们使用_nativeInstancePointer作为token,这是一种常见的做法。如果externalAddress本身是一个 Dart 对象,那么token必须是不同的对象,以避免循环引用导致managedObject永远无法被回收。
3. detach 方法
NativeFinalizer 还提供了一个 detach 方法,允许在 Dart 对象被 GC 之前手动取消其与 NativeFinalizer 的关联。
void detach(Object managedObject)
managedObject(类型Object):
需要解除关联的 Dart 对象。一旦调用detach,即使managedObject被 GC,NativeFinalizer也不会再触发其回调。
detach 方法在实现显式 dispose() 模式时非常关键。当用户显式调用 dispose() 释放 C++ 资源后,我们就需要 detach 终结器,否则当 Dart 对象最终被 GC 时,终结器可能会再次尝试释放已经释放的 C++ 资源,导致双重释放错误(double free)。
4. 生命周期图示
理解 NativeFinalizer 最好的方式是将其融入到 Dart VM 的生命周期管理中:
+----------------+ +------------------+ +-----------------------+
| Dart Object | | NativeFinalizer | | Finalizer Thread |
| (e.g., MyNativeObject)| | Instance | | (Dart VM internal) |
+----------------+ +------------------+ +-----------------------+
| | ^
| 1. Constructor | |
| creates | |
V | |
[Dart Object Instance] | |
| | |
| 2. `_finalizer.attach(this, nativePtr, ...)` |
| | |
+-------------------> [NativeFinalizer Entry] <-----+ 3. C++ object pointer (nativePtr)
| (managedObject: this, | is stored internally
| externalAddress: nativePtr) |
| |
| |
V |
+-------------------------------------------------+
| Dart VM Garbage Collector (GC) |
+-------------------------------------------------+
| |
| 4. `managedObject` becomes unreachable (out of scope, no references)
| |
| 5. GC detects `managedObject` is eligible for collection
| |
V |
+-------------------------------------------------+
| GC schedules finalization task |
| (passes `nativePtr` to finalizer thread) |
+-------------------------------------------------+
|
V
(Eventually, on a separate Finalizer Thread)
|
| 6. Finalizer thread picks up task
|
V
+---------------------------------------+
| Call `_destroyMyNativeClassPointer` |
| with `nativePtr` as argument |
+---------------------------------------+
|
V
+-------------------+
| C++ `destroy_my_native_class` |
| function executes |
| `delete nativePtr;` |
+-------------------+
|
V
C++ Object Memory Freed
这个流程图展示了从 Dart 对象创建、附加终结器,到 Dart 对象被 GC、终结器线程调用 C++ 析构函数的过程。理解这个流程对于正确地利用 NativeFinalizer 进行资源管理至关重要。
综合案例:一个简单的 C++ 向量类
为了进一步巩固理解,我们来看一个更实际的例子:一个简单的 C++ 向量类,它在内部管理一个动态分配的整数数组。
C++ Vector 类的定义与实现
我们将创建一个 MyVector 类,它封装了一个动态整数数组,并提供一些基本操作。
my_vector.h
#ifndef MY_VECTOR_H
#define MY_VECTOR_H
#include <cstddef> // For size_t
#include <iostream>
class MyVector {
public:
MyVector(size_t size, int initialValue);
~MyVector();
// Copy constructor and assignment operator are deleted to simplify ownership
// and prevent accidental deep copies that FFI might not handle correctly.
MyVector(const MyVector&) = delete;
MyVector& operator=(const MyVector&) = delete;
size_t getSize() const;
int getValue(size_t index) const;
void setValue(size_t index, int value);
void print() const;
private:
int* data_;
size_t size_;
};
// C-style interface for Dart FFI
extern "C" {
// Factory function to create a MyVector instance
MyVector* create_my_vector(size_t size, int initialValue);
// Get the size of the vector
size_t my_vector_get_size(MyVector* vec);
// Get a value at a specific index
int my_vector_get_value(MyVector* vec, size_t index);
// Set a value at a specific index
void my_vector_set_value(MyVector* vec, size_t index, int value);
// Print the vector content
void my_vector_print(MyVector* vec);
// Destructor function to destroy a MyVector instance
void destroy_my_vector(MyVector* vec);
} // extern "C"
#endif // MY_VECTOR_H
my_vector.cpp
#include "my_vector.h"
#include <algorithm> // For std::fill
MyVector::MyVector(size_t size, int initialValue) : size_(size) {
if (size_ == 0) {
data_ = nullptr;
} else {
data_ = new int[size_];
std::fill(data_, data_ + size_, initialValue);
}
std::cout << "[C++] MyVector created with size " << size_ << ", initial value " << initialValue << "." << std::endl;
}
MyVector::~MyVector() {
if (data_) {
delete[] data_;
data_ = nullptr; // Defensive programming
}
std::cout << "[C++] MyVector destructed (size: " << size_ << ")." << std::endl;
}
size_t MyVector::getSize() const {
return size_;
}
int MyVector::getValue(size_t index) const {
if (index >= size_ || data_ == nullptr) {
std::cerr << "[C++] Error: Index " << index << " out of bounds or vector is null/empty (size: " << size_ << ")." << std::endl;
return -1; // Indicate error
}
return data_[index];
}
void MyVector::setValue(size_t index, int value) {
if (index >= size_ || data_ == nullptr) {
std::cerr << "[C++] Error: Index " << index << " out of bounds or vector is null/empty (size: " << size_ << ")." << std::endl;
return;
}
data_[index] = value;
}
void MyVector::print() const {
std::cout << "[C++] MyVector content [";
for (size_t i = 0; i < size_; ++i) {
std::cout << data_[i] << (i == size_ - 1 ? "" : ", ");
}
std::cout << "]" << std::endl;
}
// C-style interface implementation
extern "C" {
MyVector* create_my_vector(size_t size, int initialValue) {
return new MyVector(size, initialValue);
}
size_t my_vector_get_size(MyVector* vec) {
if (!vec) {
std::cerr << "[C++] Error: my_vector_get_size called with null vector." << std::endl;
return 0;
}
return vec->getSize();
}
int my_vector_get_value(MyVector* vec, size_t index) {
if (!vec) {
std::cerr << "[C++] Error: my_vector_get_value called with null vector." << std::endl;
return -1;
}
return vec->getValue(index);
}
void my_vector_set_value(MyVector* vec, size_t index, int value) {
if (!vec) {
std::cerr << "[C++] Error: my_vector_set_value called with null vector." << std::endl;
return;
}
vec->setValue(index, value);
}
void my_vector_print(MyVector* vec) {
if (!vec) {
std::cerr << "[C++] Error: my_vector_print called with null vector." << std::endl;
return;
}
vec->print();
}
void destroy_my_vector(MyVector* vec) {
if (vec) {
delete vec;
} else {
std::cerr << "[C++] Warning: destroy_my_vector called with null object." << std::endl;
}
}
} // extern "C"
CMakeLists.txt (更新以包含 my_vector.cpp)
cmake_minimum_required(VERSION 3.10)
project(my_native_library CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(SOURCES
my_native_class.cpp # Keep the previous class
my_vector.cpp # Add the new vector class
)
add_library(my_native_library SHARED ${SOURCES})
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
重新构建你的共享库:mkdir build && cd build && cmake .. && cmake --build .
Dart MyVector 包装类的实现
import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:ffi/ffi.dart';
// --- FFI Setup for my_native_library (same as before) ---
final String _libraryPath = Platform.isMacOS
? 'build/lib/libmy_native_library.dylib'
: Platform.isLinux
? 'build/lib/libmy_native_library.so'
: Platform.isWindows
? 'build/lib/my_native_library.dll'
: throw Exception('Unsupported platform');
final DynamicLibrary myNativeLib = DynamicLibrary.open(_libraryPath);
// --- FFI Function Signatures for MyVector ---
// MyVector* create_my_vector(size_t size, int initialValue);
typedef Pointer<Void> CreateMyVector_Native(Uint64 size, Int32 initialValue);
typedef Pointer<Void> CreateMyVector_Dart(int size, int initialValue);
// size_t my_vector_get_size(MyVector* vec);
typedef Uint64 MyVectorGetSize_Native(Pointer<Void> vec);
typedef int MyVectorGetSize_Dart(Pointer<Void> vec);
// int my_vector_get_value(MyVector* vec, size_t index);
typedef Int32 MyVectorGetValue_Native(Pointer<Void> vec, Uint64 index);
typedef int MyVectorGetValue_Dart(Pointer<Void> vec, int index);
// void my_vector_set_value(MyVector* vec, size_t index, int value);
typedef Void MyVectorSetValue_Native(Pointer<Void> vec, Uint64 index, Int32 value);
typedef void MyVectorSetValue_Dart(Pointer<Void> vec, int index, int value);
// void my_vector_print(MyVector* vec);
typedef Void MyVectorPrint_Native(Pointer<Void> vec);
typedef void MyVectorPrint_Dart(Pointer<Void> vec);
// void destroy_my_vector(MyVector* vec);
typedef Void DestroyMyVector_Native(Pointer<Void> vec);
typedef void DestroyMyVector_Dart(Pointer<Void> vec);
// --- Lookup FFI Functions for MyVector ---
final CreateMyVector_Dart _createMyVector = myNativeLib
.lookupFunction<CreateMyVector_Native, CreateMyVector_Dart>(
'create_my_vector');
final MyVectorGetSize_Dart _myVectorGetSize = myNativeLib
.lookupFunction<MyVectorGetSize_Native, MyVectorGetSize_Dart>(
'my_vector_get_size');
final MyVectorGetValue_Dart _myVectorGetValue = myNativeLib
.lookupFunction<MyVectorGetValue_Native, MyVectorGetValue_Dart>(
'my_vector_get_value');
final MyVectorSetValue_Dart _myVectorSetValue = myNativeLib
.lookupFunction<MyVectorSetValue_Native, MyVectorSetValue_Dart>(
'my_vector_set_value');
final MyVectorPrint_Dart _myVectorPrint = myNativeLib
.lookupFunction<MyVectorPrint_Native, MyVectorPrint_Dart>(
'my_vector_print');
final Pointer<NativeFinalizerFunction> _destroyMyVectorPointer = myNativeLib
.lookup<NativeFinalizerFunction>('destroy_my_vector');
// --- Dart MyVector Wrapper Class ---
class MyVector {
late Pointer<Void> _nativeInstancePointer;
static final NativeFinalizer _finalizer =
NativeFinalizer(_destroyMyVectorPointer);
MyVector(int size, {int initialValue = 0}) {
if (size < 0) {
throw ArgumentError('Vector size cannot be negative.');
}
_nativeInstancePointer = _createMyVector(size, initialValue);
if (_nativeInstancePointer == nullptr) {
throw Exception('Failed to create native MyVector instance.');
}
_finalizer.attach(this, _nativeInstancePointer, token: _nativeInstancePointer);
print('[Dart] MyVector(size: $size, initialValue: $initialValue) created and finalizer attached.');
}
int get size {
if (_nativeInstancePointer == nullptr) {
throw StateError('Cannot access size of a disposed vector.');
}
return _myVectorGetSize(_nativeInstancePointer);
}
int operator [](int index) {
if (_nativeInstancePointer == nullptr) {
throw StateError('Cannot access elements of a disposed vector.');
}
if (index < 0 || index >= size) {
throw RangeError.index(index, this, 'index', null, size);
}
return _myVectorGetValue(_nativeInstancePointer, index);
}
void operator []=(int index, int value) {
if (_nativeInstancePointer == nullptr) {
throw StateError('Cannot modify elements of a disposed vector.');
}
if (index < 0 || index >= size) {
throw RangeError.index(index, this, 'index', null, size);
}
_myVectorSetValue(_nativeInstancePointer, index, value);
}
void printVector() {
if (_nativeInstancePointer == nullptr) {
print('[Dart] Cannot print a disposed vector.');
return;
}
_myVectorPrint(_nativeInstancePointer);
}
void dispose() {
if (_nativeInstancePointer != nullptr) {
print('[Dart] MyVector (size: ${size}) explicitly disposing.');
_finalizer.detach(this);
_destroyMyVectorPointer.asFunction<DestroyMyVector_Dart>()(_nativeInstancePointer);
_nativeInstancePointer = nullptr;
print('[Dart] MyVector explicitly disposed.');
} else {
print('[Dart] Attempted to dispose already disposed MyVector (no-op).');
}
}
}
// --- Main application logic ---
void main() async {
print('--- Starting MyVector FFI RAII Example ---');
// Scenario 1: Implicit cleanup
print('nScenario 1: Implicit cleanup of MyVector');
{
MyVector vec1 = MyVector(5, initialValue: 10);
vec1.printVector();
vec1[2] = 25;
vec1.printVector();
} // vec1 becomes eligible for GC here
await Future.delayed(Duration(milliseconds: 100));
print('Dart is trying to collect garbage for vec1...');
for (int i = 0; i < 100000; i++) {
List<int> dummy = List.filled(1000, i);
}
await Future.delayed(Duration(seconds: 1)); // Give finalizer thread time
// Scenario 2: Explicit cleanup
print('nScenario 2: Explicit cleanup of MyVector');
MyVector vec2 = MyVector(3, initialValue: 100);
vec2.printVector();
print('Value at index 0: ${vec2[0]}');
vec2.dispose(); // Explicitly release C++ memory
try {
vec2.printVector(); // Should indicate disposed
print('Size after dispose: ${vec2.size}'); // Should throw StateError
} catch (e) {
print('Caught error after dispose: $e');
}
// Scenario 3: Another implicit cleanup example
print('nScenario 3: Another implicit cleanup of MyVector');
MyVector vec3 = MyVector(7, initialValue: 7);
vec3.printVector();
// No explicit dispose, will rely on NativeFinalizer
print('nWaiting for potential finalizer calls for remaining objects...');
await Future.delayed(Duration(seconds: 2));
for (int i = 0; i < 100000; i++) {
List<int> dummy = List.filled(1000, i);
}
await Future.delayed(Duration(seconds: 1));
print('--- MyVector FFI RAII Example Finished ---');
}
这个 MyVector 例子展示了如何将一个更复杂的 C++ 类(带有动态内存分配)安全地集成到 Dart 中,同时利用 NativeFinalizer 和显式 dispose 方法来管理其生命周期。通过重载 [] 和 []= 运算符,我们甚至可以使 Dart 包装器在使用上更贴近 Dart 的原生 List。
高级考量与最佳实践
将 C++ 对象引入 Dart FFI 绝非仅仅是传递指针那么简单。为了构建健壮、高效且安全的应用程序,还需要考虑以下高级主题和最佳实践。
错误处理与异常传播
C++ 代码可能会抛出异常或返回错误码。Dart 无法直接捕获 C++ 异常。常见的 FFI 错误处理策略包括:
- 返回状态码:C 函数返回一个整数状态码(例如,0 表示成功,非零表示错误码)。Dart 侧检查这个码并根据需要抛出 Dart 异常。
// C++ int create_my_object_safe(int id, MyObject** out_obj); // Returns 0 on success, error code on failure// Dart int status = _createMyObjectSafe(id, objPtrPtr); if (status != 0) { throw NativeException('Failed to create object, error: $status'); } - 返回错误消息字符串:C 函数返回一个指向 C 字符串的指针,如果发生错误,该字符串包含错误描述。Dart 负责将 C 字符串转换为 Dart 字符串并根据需要释放 C 字符串(如果 C++ 分配了新的内存)。
// C++ const char* get_error_message(); // Returns last error message MyObject* create_my_object_with_error(int id); // Returns nullptr on error// Dart Pointer<Void> objPtr = _createMyObjectWithError(id); if (objPtr == nullptr) { Pointer<Utf8> errMsgPtr = _getErrorMessage(); String errMsg = errMsgPtr.toDartString(); malloc.free(errMsgPtr); // Assuming C++ allocated this for Dart to free throw NativeException('Failed to create object: $errMsg'); } - C++ 异常封装:在 C++ FFI 层捕获 C++ 异常,并将其转换为 C 风格的错误码或错误消息返回给 Dart。
// C++ extern "C" { // Function to create, returns nullptr on exception MyObject* create_object_robust(int value, char** error_message) { try { // ... C++ logic ... return new MyObject(value); } catch (const std::exception& e) { *error_message = strdup(e.what()); // Allocate C-string for error return nullptr; } } }// Dart final Pointer<Pointer<Utf8>> errorMsgPtrPtr = calloc<Pointer<Utf8>>(); final Pointer<Void> objPtr = _createObjectRobust(42, errorMsgPtrPtr); if (objPtr == nullptr) { final String errorMessage = errorMsgPtrPtr.value.toDartString(); malloc.free(errorMsgPtrPtr.value); // Free C-string malloc.free(errorMsgPtrPtr); throw Exception('Native error: $errorMessage'); }
所有权语义
在 FFI 边界传递指针时,明确所有权语义至关重要,以避免内存泄漏或双重释放。
- Dart 拥有 C++ 对象:当 C++ 创建对象并将其指针返回给 Dart 时,Dart 成为该对象的所有者。Dart 必须负责在不再需要时通过
NativeFinalizer或dispose()调用 C++ 析构函数。 - C++ 拥有 C++ 对象:如果 Dart 将一个指针传递给 C++ 函数,并且 C++ 函数将该指针存储起来以供将来使用,那么 C++ 可能成为该对象的共同所有者或唯一所有者。在这种情况下,Dart 不应尝试释放该对象,除非 C++ 明确表示它已经放弃了所有权。
- 临时指针:例如,将 Dart 字符串转换为 C 字符串 (
toNativeUtf8()),通常由 Dart 负责在 FFI 调用后立即释放 (malloc.free()),因为 C++ 函数可能只会读取其内容而不会保留其指针。
线程安全
NativeFinalizer 的回调函数在一个独立的终结器线程上执行。如果 C++ 对象被多个 Dart 隔离区或通过 FFI 从多个 Dart 线程访问,那么 C++ 析构函数必须是线程安全的。
- C++ 侧的并发控制:如果 C++ 对象在析构过程中访问共享资源或执行复杂操作,需要确保这些操作是线程安全的(例如,使用互斥锁
std::mutex)。 - Dart 侧的隔离区:Dart
Isolate之间不共享内存。如果将 C++ 对象指针传递给另一个Isolate,需要确保该Isolate也正确管理其生命周期,或者确保 C++ 库本身支持跨线程访问。对于NativeFinalizer来说,它在终结器线程上运行,与创建 Dart 对象的Isolate是不同的。
显式清理与隐式清理的平衡
如前所述,NativeFinalizer 提供了隐式、非确定性的清理。为了应对其局限性,提供一个显式的 dispose() 方法是最佳实践。
| 特性 | 手动管理 (delete by hand) |
NativeFinalizer (隐式) |
dispose() (显式) |
|---|---|---|---|
| 何时清理 | 开发者代码明确调用 | Dart 对象被 GC 回收时 | 开发者代码明确调用 |
| 确定性 | 高(由开发者控制) | 低(非确定性) | 高(由开发者控制) |
| 防内存泄漏 | 易出错(忘记调用) | 良好(自动触发) | 良好(开发者负责,但可配合 NativeFinalizer) |
| 防双重释放 | 易出错(重复调用) | 可能(如果同时有 dispose() 且未 detach) |
良好(通过 _nativeInstancePointer = nullptr 和 detach) |
| 资源类型 | 任何 | 内存资源,以及其他可由 C 函数清理的资源 | 任何 |
| 异常安全 | 依赖于手动处理 | 终结器回调函数必须是异常安全的(C++ 侧) | 开发者负责在 Dart 侧处理异常 |
| 应用程序关闭 | 如果未调用,资源将泄露 | 不保证执行 | 如果未调用,资源将泄露 |
| 开发复杂度 | 高,易出错 | 中等,需要设置 FFI 签名和包装器 | 中等,需要实现 dispose() 和 detach |
通常,推荐的模式是:
- 默认使用
NativeFinalizer:它处理了大多数忘记释放的情况。 - 提供
dispose()方法:对于需要立即释放资源、处理非内存资源(如文件句柄、网络连接)或在应用程序关闭时确保清理的场景。在dispose()中务必调用_finalizer.detach(this)来避免双重释放。 - 使
dispose()幂等:确保多次调用dispose()不会导致问题(例如,通过检查_nativeInstancePointer == nullptr)。
性能考量
FFI 调用本身有少量开销,因为它涉及从 Dart VM 到原生代码的上下文切换。对于频繁调用的简单函数,这种开销可能会累积。NativeFinalizer 的开销相对较小,因为它只在 GC 发生时触发,并且在单独的线程上运行。
- 批量操作:尽量减少 FFI 调用的数量。如果需要对大量数据进行操作,最好在 C++ 侧完成批量处理,而不是在 Dart 循环中频繁调用 FFI。
- 数据拷贝:Dart 和 C++ 之间的数据传递(特别是字符串和复杂结构体)可能涉及内存拷贝,这会影响性能。尽量通过指针共享数据(但要小心所有权),或者使用
TypedData(Uint8List等) 和Pointer<Uint8>来高效传递二进制数据。
调试技巧
调试 FFI 和内存问题可能非常棘手:
- 日志输出:在 C++ 构造函数、析构函数和 FFI 包装函数中添加详细的日志输出,可以帮助跟踪对象的生命周期和函数调用。
- AddressSanitizer (ASan):在 C++ 编译时启用 ASan (例如,
g++ -fsanitize=address ...) 是检测内存错误(如 Use-After-Free, Double Free, 越界访问)的强大工具。 - Valgrind (Linux):Valgrind 可以检测内存泄漏和内存错误,对于调试 C/C++ 内存问题非常有用。
- Dart DevTools:虽然 DevTools 主要针对 Dart 内存,但它可以帮助你观察 Dart 对象的生命周期,进而推断
NativeFinalizer是否应该被触发。 - 空指针检查:在 C++ 侧对传入的指针进行空检查,并在 Dart 侧对返回的指针进行空检查,以避免解引用无效内存。
更健壮的 Dart 包装器设计
为了使 Dart 包装器更健壮、更符合 Dart 最佳实践,我们可以做一些改进:
- 使用
final字段:对于_nativeInstancePointer和_finalizer,它们在对象生命周期内不会改变,应声明为final。 - 空安全:确保在访问
_nativeInstancePointer之前进行非空检查,尤其是在dispose之后。 - 幂等性
dispose:确保dispose()方法可以安全地被多次调用而不会导致错误。 - 接口或抽象类:如果有很多 FFI 包装器,可以考虑定义一个
Disposable接口。
// 定义一个可释放资源的接口
abstract interface class Disposable {
void dispose();
bool get isDisposed;
}
class MyNativeObject implements Disposable {
// `late final` 确保在构造函数中初始化一次,之后不可变
late final Pointer<Void> _nativeInstancePointer;
static final NativeFinalizer _finalizer =
NativeFinalizer(_destroyMyNativeClassPointer);
// 跟踪是否已手动释放
bool _isDisposed = false;
MyNativeObject(int id, String name) {
final Pointer<Utf8> namePtr = name.toNativeUtf8();
_nativeInstancePointer = _createMyNativeClass(id, namePtr);
malloc.free(namePtr);
if (_nativeInstancePointer == nullptr) {
throw Exception('Failed to create native MyNativeClass instance.');
}
_finalizer.attach(this, _nativeInstancePointer, token: _nativeInstancePointer);
print('[Dart] MyNativeObject($id, "$name") created and finalizer attached.');
}
// Getter for isDisposed
@override
bool get isDisposed => _isDisposed;
void greet() {
_ensureNotDisposed();
_myNativeClassGreet(_nativeInstancePointer);
}
int getId() {
_ensureNotDisposed();
return _myNativeClassGetId(_nativeInstancePointer);
}
String getName() {
_ensureNotDisposed();
final Pointer<Utf8> namePtr = _myNativeClassGetName(_nativeInstancePointer);
if (namePtr == nullptr) {
return 'Error: Native object name is null.';
}
return namePtr.toDartString();
}
@override
void dispose() {
if (_isDisposed) {
print('[Dart] Attempted to dispose already disposed MyNativeObject (no-op).');
return;
}
print('[Dart] MyNativeObject(${getId()}, "${getName()}") explicitly disposing.');
// Detach the finalizer to prevent double-free when GC happens later.
_finalizer.detach(this);
// Manually call the C++ destructor
_destroyMyNativeClassPointer.asFunction<DestroyMyNativeClass_Dart>()(_nativeInstancePointer);
// Invalidate the pointer and mark as disposed
// Note: Can't set _nativeInstancePointer = nullptr directly because it's final.
// However, the C++ object it pointed to is now invalid.
// The _isDisposed flag is crucial here.
_isDisposed = true;
print('[Dart] MyNativeObject explicitly disposed.');
}
// Helper to throw if disposed
void _ensureNotDisposed() {
if (_isDisposed) {
throw StateError('Cannot use a disposed MyNativeObject instance.');
}
}
}
注意:_nativeInstancePointer 声明为 late final 意味着它只能被赋值一次。在 dispose() 方法中,我们不能将其设为 nullptr。因此,_isDisposed 标志变得至关重要,它作为 Dart 侧的唯一真理来源,指示 C++ 资源是否已被释放。所有成员方法都应首先检查 _isDisposed。
结语
通过 Dart FFI 与 C++ 的 NativeFinalizer 机制相结合,我们成功地将 C++ 的 RAII 模式延伸到了 Dart 运行时环境。这使得在 Dart 应用中安全、高效地使用原生 C++ 对象成为可能,极大地简化了跨语言的内存管理,并有效避免了手动资源管理带来的诸多陷阱。理解 NativeFinalizer 的非确定性特性,并配合显式 dispose() 方法进行设计,是构建健壮 FFI 应用程序的关键。