Dart FFI 回调无法使用局部变量?使用 NativeCallable.isolateLocal

在 Flutter 项目中使用 Dart FFI 调用 Windows API 时,经常需要把 Dart 函数作为回调传给 Native。

例如使用 EnumDisplayMonitors 枚举显示器:

ini 复制代码
final pointer = ffi.Pointer.fromFunction<ddc.MonitorEnumProcNative>(
  _logicalMonitorsEnumProc,
  0,
);

一开始看起来没有问题,但当回调需要操作当前方法中的局部变量时,问题就出现了。

例如:

arduino 复制代码
void enumerateMonitors() {
  final monitors = <int>[];

  // 希望在回调中把显示器句柄加入 monitors
}

理想情况下,希望直接这样写:

csharp 复制代码
monitors.add(hMonitor);

但 Pointer.fromFunction 对回调函数有严格限制。

Pointer.fromFunction 的限制

使用:

ini 复制代码
ffi.Pointer.fromFunction<MonitorEnumProcNative>(
  _logicalMonitorsEnumProc,
  0,
);

传入的 _logicalMonitorsEnumProc 必须是:

  • 顶级函数
  • static 静态方法

不能直接使用普通实例方法,也不能使用需要捕获局部变量的闭包。

例如下面这种写法就无法直接作为 Pointer.fromFunction 的回调:

ini 复制代码
void enumerateMonitors() {
  final monitors = <int>[];

  final callback = (int hMonitor) {
    monitors.add(hMonitor);
  };
}

原因在于这个闭包捕获了:

复制代码
monitors

而 Pointer.fromFunction 要求的是静态或顶级函数。

于是就出现了一个实际问题:

markdown 复制代码
当前方法
    ↓
monitors
    ↓
需要传给 Native 回调
    ↓
Pointer.fromFunction
    ↓
回调不能捕获 monitors

传统解决方式:通过上下文传递数据

Windows API 的回调通常会提供一个上下文参数。

例如 EnumDisplayMonitors 的回调包含:

objectivec 复制代码
BOOL CALLBACK MonitorEnumProc(
    HMONITOR hMonitor,
    HDC hdcMonitor,
    LPRECT lprcMonitor,
    LPARAM dwData
);

其中:

复制代码
dwData

可以用来传递上下文。

于是可以设计一个 ID:

ini 复制代码
final id = _nextId++;

然后保存:

ini 复制代码
_instances[id] = this;

调用 Windows API 时把 id 作为 dwData 传进去。

回调仍然使用静态方法:

java 复制代码
static int _logicalMonitorsEnumProc(
  int hMonitor,
  int hdcMonitor,
  ffi.Pointer<ffi.Void> lprcMonitor,
  int dwData,
) {
  final instance = _instances[dwData];

  if (instance != null) {
    instance.onMonitorFound(hMonitor);
  }

  return 1;
}

这样就形成了:

javascript 复制代码
Native
  ↓
dwData
  ↓
ID
  ↓
静态 Map
  ↓
找到 Dart 对象
  ↓
调用实例方法

这种方式可以工作,但代码会增加一层状态管理。

如果只是为了让回调访问一个局部 List,就显得比较重。

Dart 3.1+:使用 NativeCallable.isolateLocal

如果 Native API 会在创建回调的 Dart isolate 中同步调用回调,可以使用:

复制代码
ffi.NativeCallable.isolateLocal

它允许回调使用闭包。

例如:

php 复制代码
void enumerateMonitors() {
  final monitors = <int>[];

  final callable =
      ffi.NativeCallable<ddc.MonitorEnumProcNative>.isolateLocal(
    (
      int hMonitor,
      int hdcMonitor,
      ffi.Pointer<ffi.Void> lprcMonitor,
      int dwData,
    ) {
      monitors.add(hMonitor);
      return 1;
    },
    exceptionalReturn: 0,
  );

  final callback = callable.nativeFunction;

  // 调用 Windows API
  // EnumDisplayMonitors(nullptr, nullptr, callback, 0);

  callable.close();
}

这里最关键的变化是:

ini 复制代码
final monitors = <int>[];

可以直接被回调使用:

csharp 复制代码
monitors.add(hMonitor);

不需要:

javascript 复制代码
static Map
ID
dwData
实例查找

闭包直接捕获了当前作用域。

为什么 NativeCallable.isolateLocal 可以做到?

可以把两种方式理解成:

scss 复制代码
Pointer.fromFunction

Native
  ↓
static / top-level Dart function
  ↓
无法捕获当前作用域

而:

复制代码
NativeCallable.isolateLocal

Native
  ↓
NativeCallable
  ↓
Dart closure
  ↓
可以捕获当前作用域

因此:

ini 复制代码
final monitors = <int>[];

final callable =
    ffi.NativeCallable<MonitorEnumProcNative>.isolateLocal(
  (hMonitor, hdcMonitor, rect, dwData) {
    monitors.add(hMonitor);
    return 1;
  },
  exceptionalReturn: 0,
);

monitors 和回调处在同一个 Dart 作用域中。

使用完必须 close

NativeCallable 创建之后,需要在不再使用时关闭:

go 复制代码
callable.close();

因此完整生命周期是:

go 复制代码
创建 NativeCallable
        ↓
获取 nativeFunction
        ↓
传给 Native API
        ↓
Native 同步执行回调
        ↓
完成
        ↓
callable.close()

不要创建后一直保留而不关闭,否则会留下 Native 侧资源。

isolateLocal 有线程限制

这里有一个非常重要的前提。

NativeCallable.isolateLocal 只能用于:

Native 在创建它的 Dart isolate 中同步调用回调。

例如:

复制代码
Dart
 ↓
调用 Windows API
 ↓
Windows API
 ↓
同步执行 callback
 ↓
返回 Dart

这种场景可以使用:

scss 复制代码
NativeCallable.isolateLocal(...)

但如果 Native API 创建了其他线程:

markdown 复制代码
Dart
 ↓
Windows API
 ↓
创建 Native 线程
        ↓
        callback

就不能简单使用 isolateLocal。

这种情况下需要考虑:

scss 复制代码
NativeCallable.listener(...)

listener 面向的是 Native 线程向 Dart isolate 发送回调的场景。

Pointer.fromFunction 还需要注意什么?

Pointer.fromFunction 并不是不能使用。

如果回调很简单,而且本身就是顶级函数:

arduino 复制代码
int monitorCallback(
  int hMonitor,
  int hdcMonitor,
  ffi.Pointer<ffi.Void> rect,
  int dwData,
) {
  return 1;
}

那么:

ini 复制代码
final callback =
    ffi.Pointer.fromFunction<MonitorEnumProcNative>(
  monitorCallback,
  0,
);

依然可以使用。

真正需要考虑的是:

markdown 复制代码
回调是否需要捕获当前作用域?
        ↓
是
        ↓
Native 是否同步在当前 isolate 调用?
        ↓
是 → NativeCallable.isolateLocal

如果 Native 回调可能来自其他线程,则需要根据 API 的调用方式选择其他方案,例如 NativeCallable.listener。

Pointer.fromFunction 适合简单的静态回调, NativeCallable.isolateLocal 更适合需要访问当前 Dart 作用域的同步 FFI 回调。

选择哪一种,关键不只是看 API 写法,还要看 Native 回调发生在哪个线程,以及回调是否需要捕获 Dart 上下文。

相关推荐
好的佩奇13 分钟前
Dart 之类修饰符
android·flutter·dart
米米商聊1 小时前
同名附件为什么被 Map 覆盖?重复键与一对多索引
前端·javascript
echome8882 小时前
Python 异常处理:try、else、finally 的正确搭配
开发语言·前端·python·异常处理·编程技巧
时间的情敌2 小时前
OpenLayers 实战|Vue3 + OpenLayers 完整工程案例(加载天地图、要素、弹窗、绘制、WFS)
前端·openlayers
陈随易2 小时前
下载神器!Motrix 停更三年后重写回归
前端·后端·程序员
前端的日常3 小时前
一个人+AI做情侣食谱小程序,0 费用的服务器
前端·后端
计算机魔术师3 小时前
谷歌搜索广告踩刹车,AI广告成为救命稻草
前端
总要学点什么3 小时前
第81篇:数据可视化-PM2.5分布——让圆点的胖瘦替每座城市报出浓度
前端·信息可视化·echarts·cesium·空间分析
IT_陈寒4 小时前
Vue的v-for里用了个临时变量,页面直接挂了
前端·人工智能·后端
hunter_0764 小时前
一次 iOS 真机签名 409 的完整排查:证书、设备、描述文件为什么总是"对不上"
前端