新闻详情

新闻详情

首页 / 资讯中心 / 详情

Wren 嵌入指南:用 foreign class 在 C 与 Wren 之间存储原生数据

发布时间:2026/9/26 3:06:13来源:尧图网络
Wren 嵌入指南:用 foreign class 在 C 与 Wren 之间存储原生数据
编程语言语言运行时编译器【免费下载链接】wrenThe Wren Programming Language. Wren is a small, fast, class-based concurrent scripting language.项目地址https://gitcode.com/gh_mirrors/wr/wren点击查看免费下载导读本文深入讲解 Wren 嵌入embedding场景中最核心的能力之一——foreign class外部类它允许你在 Wren 对象内部包裹一块由 C 管理的原始内存从而把文件句柄、数据库连接、网络 socket 等原生资源安全地封装成 Wren 对象并让用户像使用普通 Wren 类一样创建、传递和销毁它们。读完本文你将掌握foreign class的声明语法、bindForeignClassFn绑定回调的编写、wrenSetSlotNewForeign()/wrenGetSlotForeign()两个关键 API 的用法以及 finalizer 终结器机制的正确实现方式并能独立完成一个包含构造、方法和资源回收的完整 C 扩展类。本文是 Wren 嵌入文档 系列中《Storing C Data》的深度展开版与 调用 Wren 中的 C 函数foreign method、配置 VM 和 槽与句柄slot 与 handle 互为姊妹篇。为什么需要 foreign classWren 侧没有的状态Wren 是一门小、快、基于类且天然支持并发的脚本语言见 README.md。但在嵌入场景中纯 Wren 对象有两个天然短板无法直接持有原生指针。Wren 对象的内存全部由自己的 GC 管理Wren 侧没有一个指针类型无法表示FILE*、sqlite3*这类 C 资源句柄存储效率受限。如果你想保存一块二进制数据或一个紧凑的数值结构用 Wren 的 List/Map 动态表示会引入大量装箱与分配开销。foreign class 正是为解决这两类问题设计的嵌合体状态一半是 Wren、一半是 C。它是一条真实的 Wren 类拥有名字、构造器和方法——你可以为它编写 Wren 方法也可以为其绑定用 C 实现的 foreign method它产出的也是真实 Wren 对象可以在 Wren 代码中自由传递、做is类型判断。但同时它内部额外包裹了一段对 Wren 不透明、对 C 完全可见的原始字节raw bytes。从源码结构看foreign 对象在 VM 中就是这样一个普通对象头 弹性数组typedef struct { Obj obj; uint8_t data[FLEXIBLE_ARRAY]; } ObjForeign;见 src/vm/wren_value.h。data[]就是那一段藏起来的原始内存——GC 会为它分配与对象头连续的内存而 C 侧通过下文要讲的 API 拿到指向它的指针。声明一个 foreign class在 Wren 代码中用foreign关键字修饰class即可声明一个外部类foreign class Point { // ... }foreign关键字告诉 Wren当构造该类的实例时要把宿主应用程序host请进来——宿主告诉 Wren 每个外部实例需要多少额外的原始字节而 Wren 则给宿主一个机会去初始化这些数据。class 体内可以继续声明普通 Wren 方法也可以声明foreign方法见后文 File 示例。编译器眼中的 foreign class从编译器实现看foreign关键字在类声明语法中的位置是明确的词法分析中TOKEN_FOREIGN紧跟类声明src/vm/wren_compiler.c。关键的一点是foreign class 不能声明普通字段。普通类需要按字段数量numFields分配实例空间而 foreign class 的实例空间完全由 C 侧的 allocator 决定因此编译时它的numFields被固定为-1作为标记——createClass()中正是通过if (numFields -1) bindForeignClass(vm, classObj, module);来触发绑定流程src/vm/wren_vm.c。这条源码证据也印证了文档中状态一半在 Wren类的方法表、一半在 C原始字节的描述foreign class 的 Wren 侧部分只有方法与继承关系没有字段。绑定 foreign classbindForeignClassFn 回调为了与宿主应用通信Wren 需要一个当外部类实例被构造时调用的 C 函数。这个函数通过一种与 foreign method 绑定 相似的过程来查找。当你在 配置 VM 时需要把WrenConfiguration中的bindForeignClassFn字段指向你自己实现的 C 回调。该回调的签名必须是WrenForeignClassMethods bindForeignClass( WrenVM* vm, const char* module, const char* className);Wren 会在外部类声明语句被执行时调用一次该回调传入外部类所在模块名module与正在声明的类名className。宿主的职责是返回下面这个结构体typedef struct { WrenForeignMethodFn allocate; WrenFinalizerFn finalize; } WrenForeignClassMethods;类型定义见 src/include/wren.h。它是一对函数指针allocate每当外部类的一个实例被创建时Wren 都会调用它该字段必须提供finalize才是可选的finalize可选。如果提供Wren 会在 GC 即将回收某个外部实例时调用它用于释放外部资源。注意二者的函数类型并不相同allocate与普通 foreign method 一样是void (*)(WrenVM* vm)而finalize是void (*)(void* data)——区别的根源在于调用时机详见下文终结器一节。从 VM 实现看bindForeignClass会把得到的函数指针以特殊签名allocate和finalize绑定到类的方法表上src/vm/wren_vm.c。也就是说在 VM 内部allocator 与 finalizer 本质上被当作两个特殊方法处理createForeign()在构造实例时按allocate签名查找方法并直接调用src/vm/wren_vm.c。此外VM 还留了一个后门即使宿主没有绑定random这类内置可选模块的外部类也会被自动绑定src/vm/wren_vm.c所以宿主在回调里返回allocate NULL finalize NULL是安全的——Wren 会先查内置模块查不到才会报运行时错误。allocate 回调的签名allocation 回调与 foreign method 拥有相同的签名void allocate(WrenVM* vm);初始化实例wrenSetSlotNewForeign()当你通过调用某个 构造器 创建外部类实例时Wren 会调用你绑定的allocate回调。该回调的首要职责是告诉 Wren 你需要多少字节的原始内存通过下面的 API 完成void* wrenSetSlotNewForeign(WrenVM* vm, int slot, int classSlot, size_t size);声明见 src/include/wren.h。与其它 slot 槽位操作函数 一样它既读取也写入槽数组。它有三个参数参数含义在 allocate 回调中的取值slot新 foreign 对象要放入的目标槽位0allocate 回调中对象应放在槽 0classSlot被构造的 foreign 类所在的槽位0VM 调用 allocate 时类本身已在槽 0size你想让该外部实例额外存储的原始字节数你需要的字节数第三个参数size是真正值得关注的一个它决定 Wren 为这个外部实例分配多少归你玩的 C 内存。例如想要一个包含 8 字节 C 数据的实例void* data wrenSetSlotNewForeign(vm, 0, 0, 8);wrenSetSlotNewForeign()的返回值就是指向这段已分配字节的原始指针。你可以把它强转成任何尺寸合适的 C 类型只要不超出请求的字节数并按需初始化// 在 8 字节中存两个 double double* pair (double*)wrenSetSlotNewForeign(vm, 0, 0, sizeof(double) * 2); pair[0] 1.0; pair[1] 2.0;两个关键细节构造器参数就在后续槽位中。任何传给构造器的参数都会依次出现在槽数组中因此你可以基于 Wren 侧传入的值来初始化 foreign 数据。示例 test/api/foreign_class.c 中的Point类展示了这一技巧同一个 allocator 被两个构造器共用它通过wrenGetSlotCount(vm)嗅探槽位数量判断是无参默认构造还是带三个坐标的构造从而决定把坐标初始化为(0,0,0)还是取自槽 1、2、3。allocate 返回后Wren 侧的构造器函数体才会执行。之后一切都恢复正常执行流程——在 Wren 看来你拿到的就是一个普通类实例只不过它内部多藏了一段可从 foreign method 访问的字节。访问 foreign 数据wrenGetSlotForeign()访问存储在外部实例中的原始数据通常通过其它 foreign method完成。这些方法一般定义在同一个 foreign class 上但也可以定义在别的类上——Wren 对此并不关心。只要某个槽位里是一个 foreign 实例你就可以通过下面这个 API 取回它包裹的原始字节void* wrenGetSlotForeign(WrenVM* vm, int slot);声明见 src/include/wren.h。传入包含 foreign 对象的槽位索引它返回指向该对象包裹原始内存的指针。与 C API 的一贯风格一致它不做任何类型或边界检查——保证该槽位确实是 foreign 类实例、且内存足够大是宿主自己的责任。拿到 void 指针后你就可以自由读写它指向的数据它们是你的比特Wren 只是替你保管。释放资源finalizer 终结器如果你的 foreign 实例只是纯粹持有内存、并且你愿意把这段内存的生命周期完全交给 Wren 的垃圾回收器那么到这里你就已经完成了。只要实例仍被引用Wren 就会一直保留这些字节当实例不可达时GC 终会回收这块内存。但现实往往更复杂你的 foreign 数据常常引用一些需要显式管理生命周期的资源。例如一个包装了打开的文件句柄的 foreign 对象必须保证当 GC 释放实例时句柄不会被遗留在打开状态。当然你通常也应该在 foreign class 上提供一个类似close()的方法让用户显式释放对象管理的资源但如果用户忘了调用、对象又已不可达你依然希望在资源泄漏发生前兜底。为此可以在绑定外部类时提供finalizer——即WrenForeignClassMethods结构体中的另一个回调。只要提供了它Wren 就会在 GC即将释放某个外部实例前调用它给你最后一次清理资源的机会。为什么 finalizer 的签名如此受限前面提到 finalizer 与 allocator 的函数类型不同这并非随意设计finalizer 是在垃圾回收进行到一半时被调用的。此时你无法像普通 foreign method 那样随意摆弄槽位、调用 API——GC 正在移动/整理堆内存此时操作 VM 会把 Wren 置于一个危险的状态。因此 finalizer 的签名精简到极致void finalize(void* data);Wren 只把指向你 foreign 内存的指针交给你仅此而已。finalizer 里唯一应该做的事是释放该内存所引用的外部资源。从 VM 源码看回收外部对象时正是把foreign-data直接传给这个函数指针src/vm/wren_vm.c没有任何 VM 状态可供你访问这从机制上保证了 finalizer 无法干扰 GC。值得注意即使某个类没有绑定 finalizerVM 也会把finalize符号预置进符号表只是不绑定函数——这样 GC 在遍历所有外部对象时可以用一次统一的查找快速跳过无终结器的类见 src/vm/wren_vm.c 与 wren_vm.h 附近的实现注释。完整示例包装 C 标准文件 API 的 File 类把以上知识点串起来我们用文档中的经典案例——一个包装 C 标准文件 API 的File类——走一遍完整流程。1. Wren 侧声明foreign class File { construct create(path) {} foreign write(text) foreign close() }用户可以用路径创建一个文件对象向其中写入文本并在需要时显式关闭它同时我们还必须保证即使用户忘记关闭、对象被 GC 回收文件也会被关闭。2. 设置 VM宿主侧首先初始化配置挂上两个绑定回调#include wren.h int main(int argc, const char* argv[]) { WrenConfiguration config; wrenInitConfiguration(config); config.bindForeignClassFn bindForeignClass; config.bindForeignMethodFn bindForeignMethod; WrenVM* vm wrenNewVM(config); wrenInterpret(vm, my_module, some code...); return 0; }务必先调用wrenInitConfiguration(config)填充默认值以免未来WrenConfiguration新增字段时拿到未初始化的配置详见 配置 VM。bindForeignMethodFn的完整说明见 调用 C 函数。3. 绑定 foreign classbindForeignClass根据类名决定返回哪一对 allocate/finalize 函数WrenForeignClassMethods bindForeignClass( WrenVM* vm, const char* module, const char* className) { WrenForeignClassMethods methods; if (strcmp(className, File) 0) { methods.allocate fileAllocate; methods.finalize fileFinalize; } else { // Unknown class. methods.allocate NULL; methods.finalize NULL; } return methods; }分配函数负责创建实例并初始化文件句柄#include stdio.h #include wren.h void fileAllocate(WrenVM* vm) { FILE** file (FILE**)wrenSetSlotNewForeign(vm, 0, 0, sizeof(FILE*)); const char* path wrenGetSlotString(vm, 1); *file fopen(path, w); }注意这里的双重指针我们请求sizeof(FILE*)字节用来存放一个FILE*句柄wrenSetSlotNewForeign()返回指向这段内存的指针而这段内存本身存的是指针所以强转结果是FILE**。大多数情况下你只会遇到单层指针这里因为文件句柄本身也是指针才多了一层。构造函数传入的path位于槽 1槽 0 是新实例用wrenGetSlotString()读出后交给fopen()打开文件再写回 foreign 内存。finalizer 把数据还原成正确类型并关闭文件void fileFinalize(void* data) { closeFile((FILE**) data); }配合这个小工具函数static void closeFile(FILE** file) { // Already closed. if (*file NULL) return; fclose(*file); *file NULL; }它会在文件未关闭时执行fclose()并把句柄置空避免关闭后误用。*file NULL的判断在两种场景下都有价值finalizer 在对象不可达时兜底关闭同时write方法也依赖这个已关闭标志来做防御性检查见下。4. 绑定 foreign methodsforeignclass部分完成之后还需要处理两个 foreignmethod。宿主通过返回下面这个查找函数告诉 VM 每个方法对应哪个 C 函数WrenForeignMethodFn bindForeignMethod(WrenVM* vm, const char* module, const char* className, bool isStatic, const char* signature) { if (strcmp(className, File) 0) { if (!isStatic strcmp(signature, write(_)) 0) { return fileWrite; } if (!isStatic strcmp(signature, close()) 0) { return fileClose; } } // Unknown method. return NULL; }Wren 会依据类和签名逐个绑定非静态方法write(_)对应fileWriteclose()对应fileClose。绑定只发生一次类声明首次执行时之后调用直接走缓存的函数指针因此调用开销很小。写入方法void fileWrite(WrenVM* vm) { FILE** file (FILE**)wrenGetSlotForeign(vm, 0); // Make sure the file is still open. if (*file NULL) { wrenSetSlotString(vm, 0, Cannot write to a closed file.); wrenAbortFiber(vm, 0); return; } const char* text wrenGetSlotString(vm, 1); fwrite(text, sizeof(char), strlen(text), *file); }用wrenGetSlotForeign()取出槽 0方法接收者自身里的 foreign 数据并强转为FILE**先做防御检查——如果用户对已关闭的文件调用write就在槽 0 放入错误消息并wrenAbortFiber()中止当前 fiber否则用fwrite()写入文本。关闭方法则直接复用上面的closeFile助手void fileClose(WrenVM* vm) { FILE** file (FILE**)wrenGetSlotForeign(vm, 0); closeFile(file); }5. Wren 侧使用至此一个带 finalizer 和两个 foreign method 的完整外部类就完成了。在 Wren 里可以这样用var file File.create(some/path.txt) file.write(some text) file.close()从用户视角看File与普通 Wren 类毫无二致——它看起来、用起来都像原生类却拥有 C 代码的功能和大部分性能。源码与测试佐证不止于文档示例本文描述的机制不是纸面设想仓库中有真实的实现与测试互为印证API 声明src/include/wren.h 定义了WrenForeignClassMethods与两个函数指针类型src/include/wren.h 定义了wrenSetSlotNewForeign()src/include/wren.h 定义了wrenGetSlotForeign()绑定与调用流程src/vm/wren_vm.c 展示bindForeignClass如何把 allocate/finalize 以allocate、finalize特殊签名写入类方法表src/vm/wren_vm.c 展示createForeign()在构造时如何把构造参数原样交给 allocator对应文档构造器参数位于后续槽位的说明src/vm/wren_vm.c 展示 GC 回收前调用 finalizer 的过程对象表示src/vm/wren_value.h 的ObjForeign结构体Obj对象头 data[FLEXIBLE_ARRAY]就是原始字节藏在对象内部的底层形态测试用例test/api/foreign_class.c 覆盖了多种外部类形态——Counter展示最基本的sizeof(double)内存读写Point展示同一 allocator 服务多个构造器并通过wrenGetSlotCount()区分参数Resource展示 finalizer 在 GC 回收时被调用且能拿到正确的 data 指针通过exit(1)校验数据正确性BadClass则演示了 allocator 中出错时如何wrenAbortFiber()中止。对应的 Wren 侧用例见 test/api/foreign_class.wren。最佳实践与注意事项基于文档与源码可以提炼出以下实战要点allocate必须调用wrenSetSlotNewForeign()且只调一次。头文件注释明确要求Inside the body of this, it must callwrenSetSlotNewForeign()exactly oncesrc/include/wren.h。size要留足余量。请用sizeof(T)或sizeof(T) * n表达需求不要手写魔法数字C API 不做边界检查越界访问是未定义行为。allocator 中构造参数从槽 1 开始读取槽 0 是正在构造的对象/类。多构造器共用一个 allocator 时可用wrenGetSlotCount()区分如Point示例。finalizer 只做资源释放绝不触碰 VM。它没有WrenVM*参数是有意为之——GC 中途操作 VM 会让 Wren 处于异常状态。释放后把指针置空*file NULL是防止重复释放与误用的好习惯。显式关闭与 finalizer 兜底双保险。文档建议通常应该提供close()之类的方法让用户主动释放同时用 finalizer 防止泄漏——这正是File示例展示的完整模式。绑定回调要保持幂等与完备。bindForeignClass/bindForeignMethod对未知类/方法应返回NULLVM 允许内置模块如random在宿主未绑定时自动接管绑定因此返回NULL是安全且必要的。Wren 侧的 foreign class 不声明字段。从createClass()的numFields -1分支可以看出字段声明会被编译期拒绝所有状态都放在 C 内存中由 foreign method 访问。把 foreign class 与 foreign method 配合使用你就能在 Wren 中无痛接入任意 C 生态前者负责容纳原生状态后者负责操作原生行为。二者结合正是 Wren 作为嵌入脚本语言连接宿主应用与原生世界的桥梁。赞分享编程语言语言运行时编译器【免费下载链接】wrenThe Wren Programming Language. Wren is a small, fast, class-based concurrent scripting language.项目地址https://gitcode.com/gh_mirrors/wr/wren点击查看免费下载相关推荐Wren 嵌入指南深入理解 Slots 与 Handles —— 在 C 与 Wren 之间安全传递数据Wren 嵌入指南深入理解 Slots 与 Handles —— 在 C 与 Wren 之间安全传递数据 wrenInterpret 只能让 Wren 在虚拟编程语言语言运行时编译器amis 如何用 Log 组件实时展示后端流式输出日志amis 如何用 Log 组件实时展示后端流式输出日志 amis 的 Log 组件用于实时显示日志或程序输出结果。典型场景是后端命令、任务或脚本正在持续产生输编程语言语言运行时编译器Wren 嵌入指南从 C 语言调用 Wren 方法wrenCall 实战Wren 嵌入指南从 C 语言调用 Wren 方法wrenCall 实战 本文面向需要把 Wren 作为嵌入式脚本语言集成进 C/C 主程序的开发者编程语言语言运行时编译器上一篇Layerdivider3分钟将任何图片变成可编辑PSD图层的终极指南 下一篇如何用AI快速将任何图片转换为可编辑的PSD分层文件完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

基于CNN的花卉识别项目实战:从源码到部署的完整指南 2026/9/26 3:51:39

基于CNN的花卉识别项目实战:从源码到部署的完整指南

简介:这份资源面向Python与深度学习入门者、计算机视觉方向的学生及需要完成毕业设计或课程大作业的开发者,提供一套基于卷积神经网络CNN的花卉识别完整项目,帮助读者理解图像分类从数据准备到模型推理的全流程。压缩包共48个文件&#xff0c…

阅读更多 →
第七节.水利水闸建模:倾斜摄影+FME转FBX导入UE的TaoToken配置实战 2026/9/26 3:51:33

第七节.水利水闸建模:倾斜摄影+FME转FBX导入UE的TaoToken配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
构建自动化代码审查机器人:Cursor + Claude API + GitHub App 实战(TaoToken 统一 Key 配置版) 2026/9/26 3:51:33

构建自动化代码审查机器人:Cursor + Claude API + GitHub App 实战(TaoToken 统一 Key 配置版)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
AI Agent 记忆机制全景对比:OpenClaw vs QwenPaw vs Hermes vs HiClaw 的配置骨架与 TaoToken 接入实践 2026/9/26 3:51:33

AI Agent 记忆机制全景对比:OpenClaw vs QwenPaw vs Hermes vs HiClaw 的配置骨架与 TaoToken 接入实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
数据库游标配 TaoToken:SQL 分页查询的 settings.json 骨架与验证 2026/9/26 3:51:33

数据库游标配 TaoToken:SQL 分页查询的 settings.json 骨架与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
SolidWorks打开STP弹出大量小窗口的根源与四层防御方案 2026/9/26 3:51:33

SolidWorks打开STP弹出大量小窗口的根源与四层防御方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉