
JaccwabytWASM 环境下 JavaScript 与 C 结构体双向通信实战指南【免费下载链接】libsqllibSQL is a fork of SQLite that is both Open Source, and Open Contributions.项目地址: https://gitcode.com/GitHub_Trending/li/libsql导读Jaccwabyt 是 libSQLSQLite 开源分支WASM 构建体系中一个纯 JavaScript 实现的轻量框架它为 WASM 编译后的 C 结构体建立 JavaScript 代理绑定JS 侧对结构体成员的读写会通过一块共享的 WASM 堆内存完成编码与解码从而使 JS 与 C 两侧对同一结构体状态的修改互相可见。本文以仓库中的 jaccwabyt.md 为骨架结合其单文件实现 jaccwabyt.js 与 SQLite WASM API 集成代码系统讲解它的设计动机、四步绑定流程、成员签名体系、完整 API 参考与配套的 C 结构描述生成方案。读完本文你将掌握如何在自己的 WASM/JS 混合应用中用少量代码完成 C 结构体的双向绑定与内存管理。概述为什么要为 WASM 中的 C 结构体做双向绑定Jaccwabyt 的名字是 JavaScript ⇄ C Struct Communication via WASM Byte Arrays 的缩写其核心定位如下这是一个JavaScript-only的框架为 C 结构体与 JavaScript 对象之间提供有限的双向绑定在一个环境中对结构体状态的修改在另一个环境中可见。它的工作原理可以概括为为 C 结构体创建 JavaScript 代理Proxy。JS 侧对成员属性的读写全部被编组marshaled到一块从 WASM 堆中分配出来的扁平字节数组上。由于该堆内存与 C 侧代码共享且这块内存的写入方式与 C 一致因此 JS 与 C 可以经由这块字节数组访问并操作同一个结构体实例。催生它的动机这个 API 最初是作为一次实验开发的用来判断能否完全用 JS 为 sqlite3 的 WASM 构建实现自定义的 VFS 和 virtual table 对象。这需要多个结构体的双向绑定。当概念验证成功后作者就掉进了兔子洞它已从朴素的 PoC 成长为可用于混合 JS/C 应用的实用工具。在 libSQL 仓库中Jaccwabyt 的实际地位可以从 ext/wasm/api/README.md 的构建说明看到它被作为 WASM API 单文件构建链的一部分sqlite3-api-prologue.js→whwasmutil.js→jaccwabyt.js→ ...并在 sqlite3-api-glue.c-pp.js 中被实例化为sqlite3.StructBindersqlite3.StructBinder globalThis.Jaccwabyt({ heap: 0 ? wasm.memory : wasm.heap8u, alloc: wasm.alloc, dealloc: wasm.dealloc, bigIntEnabled: wasm.bigIntEnabled, memberPrefix: /* 不要修改 */ $ }); delete globalThis.Jaccwabyt;可移植性要点文档有时以Emscripten作为参照因为它是应用最广的 WASM 工具链但 Jaccwabyt专为任意 WASM 环境设计把少数 Emscripten 特有功能抽象成了可配置项。构建树需要 Emscripten但库本身对 Emscripten没有硬性依赖。整个代码被封装在单个 JavaScript 函数中见 jaccwabyt.js 的globalThis.Jaccwabyt function StructBinderFactory(config){...}可以轻松复制/粘贴到任意使用 WASM/JS 的项目中。源码树中包含 C 代码但仅用于测试和演示不属于核心发布物。浏览器兼容性该库要求较新的浏览器并不打算兼容较老或能力较弱的浏览器。较新大致指2018 年年中之后发布的版本部分可选特性例如 Safari 中的BigInt64Array需要2021 年末的版本。它还依赖两个非标准但广泛支持的特性[TextEncoder][] 与 [TextDecoder][]。主要开发环境是 Linux 上的 Firefox 与 ChromeSafari 兼容性仅基于 MDN 特性兼容表推断。许可与出处文档与其所描述的软件使用与 sqlite3 相同的许可。2022-06-30 起作者放弃该源代码的版权代之以一段祝福May you do good and not evil. / May you find forgiveness for yourself and forgive others. / May you share freely, never taking more than you give.架构从工厂函数到结构体实例的四层对象模型文档中给出的架构图可以用文字概括为以下对象层级StructBinderFactory ──生成──▶ StructBinder ──包含──▶ StructTypeT │ │ │ │ └──生成──▶ StructT Constructor │ │ ▼ ▼ C Structs共享内存◀──────────── 实例 StructT Instances它的主要类与函数自上而下StructBinderFactory一个工厂函数接收配置对象以针对特定 WASM 环境定制行为。客户端通常只调用一次得到唯一的工厂。StructBinder一个工厂函数把任意数量的结构描述转换成构造器。StructTypes构造器每个结构描述对应一个构造器它们继承自StructBinder.StructType用于实例化对象。Struct 实例生成的各结构类型的单个实例对象。一个应用可以有任意数量的 StructBinder但通常只需一个。每个 StructBinder 实际上就是结构创建的一个独立命名空间——由不同 StructBinder 生成的对象之间互不相关除共享的工厂配置如堆内存外不共享任何状态。这一点从 jaccwabyt.js 中可看到每次调用StructBinder都会构造新的StructType与StructCtor闭包并各自维护独立的debugFlags。四步绑定流程从文档篇幅看创建和使用结构绑定似乎很复杂但本质上只有四步为你的 WASM 环境配置 Jaccwabyt每个项目一次得到一个能创建新结构绑定的工厂函数。为你的 C 结构体编写一份 JSON 格式的描述每个结构体一次C 结构体变更时需要更新。把 (2) 喂给 (1) 生成的函数为每个结构体创建 JS 构造器。这是在运行时完成的而非构建期并且可以一次性设置好、之后无需维护。创建并使用这些结构体的实例。下面逐一展开。Step 1为环境配置 JaccwabytJaccwabyt 最高层的 API 是单个函数StructBinderFactory。它创建一个用于处理结构描述的工厂但本身不处理任何描述。这个抽象层存在的意义正是为了让结构专属的工厂可以针对特定 WASM 环境进行配置。用法如下const MyBinder StructBinderFactory({ // 以下配置项都是必需的 heap: WebAssembly.Memory 实例或返回 WASM 内存的 Uint8Array / Int8Array 视图的函数 alloc: function(howMuchMemory){...}, dealloc: function(pointerToFree){...} });它还有许多其他设置但除了上面这三个其余都是可选的。这三个配置项抽象了特定 WASM 环境的细节提供 WASM 堆内存、内存分配器与释放器。在常规 Emscripten 环境下配置可能只是{ heap: Module[asm][memory], // 或者 // heap: ()Module[HEAP8], alloc: (n)Module_malloc, dealloc: (m)Module_free }工厂函数返回一个函数这个返回的函数之后就可以用来为结构体创建绑定。在 jaccwabyt.js 中可以看到config.heap必须是WebAssembly.Memory实例或函数、config.alloc与config.dealloc必须是函数否则会立即抛出异常。Step 2创建结构描述框架的主要输入是一份 JSON 兼容的构造用于描述想要绑定的结构体。例如给定如下 C 结构体// C 侧 struct Foo { int member1; void * member2; int64_t member3; };它的 JSON 描述如下{ name: Foo, sizeof: 16, members: { member1: {offset: 0, sizeof: 4, signature: i}, member2: {offset: 4, sizeof: 4, signature: p}, member3: {offset: 8, sizeof: 8, signature: j} } }这些数据必须与 C 侧的结构定义如果有完全一致。参见后文附录 G了解一种从 C 代码轻松生成这些数据的方案。members对象中每个条目都把成员名映射到其底层布局offset从结构体起始处的字节偏移由 C 的offsetof()报告。sizeof由 C 的sizeof()报告。signature见下文成员签名体系。readOnly可选。若设为true绑定层会在 JS 代码尝试给该属性赋值时抛出异常。注意members条目的顺序无关紧要内存布局由它们的offset与sizeof决定。name属性在技术上可选但绑定过程的一步要求要么给它传显式名称、要么结构描述中有名称。成员条目的名字不必与 C 侧一致。项目约定可以给 JS 侧不同的名字且 [StructBinderFactory][] 可配置为自动为它们加前缀/后缀memberPrefix/memberSuffix。嵌套结构体目前不受支持。在 jaccwabyt.js 中StructBinder会对描述做严格的健全性检查每个成员必须有sizeofsizeof1的成员签名只能是c或C其他成员的sizeof与offset必须是 4 的倍数对齐检查最后一个成员不能超出structInfo.sizeof的范围否则抛出异常。Step 3绑定结构体现在可以组合前两步的结果了const MyStruct MyBinder(myStructDescription);这会创建一个新的构造器函数MyStruct可用它来实例化新对象。绑定器遇到任何问题都会抛出异常。仅此而已。注意该函数可能为了简化后续某些操作而修改结构描述对象或其子对象甚至替换子对象。如果不希望这样就喂给它原对象的副本例如传入JSON.parse(JSON.stringify(structDefinition))。从 jaccwabyt.js 的实现看StructBinder会校验描述、创建StructCtor构造器、把StructCtor.prototype设为新的StructType实例然后对members中每个成员调用makeMemberWrapper在原型上定义对应的属性访问器getter/setter这些访问器最终通过DataView读写堆内存。Step 4创建、使用与销毁结构实例有了构造器之后const my new MyStruct();重要创建新实例会在 WASM 堆上分配内存。不能单纯依赖垃圾回收来清理实例因为 GC 不会释放 WASM 堆内存。正确释放方式是调用对象的dispose()方法。下面的用法模式提供了一种保证结构实例正确清理的简便方式const my new MyStruct(); try { console.log(my.member1, my.member2, my.member3); my.member1 12; assert(12 my.member1); /* ^^^ 这样测试看起来可能有点傻但请记住给那个属性赋值 会把值编码进堆内存中的字节数组而不是普通的 JS 属性。 同样读取该属性是从字节数组中解码出来的。 */ // 把结构体传给一个接收 MyStruct 指针的 C 函数 aCFunction( my.pointer ); } finally { my.dispose(); }无论try块如何退出——正常执行完、抛出异常还是通过return/break等流程控制关键字——finally块都会执行。完全可以在没有catch的情况下使用try/finally这与 Jaccwabyt 绑定结构实例的内存管理需求是绝配。包装已有 C 侧实例也经常用到只需把指针传给构造器就可以包装一个已有 C 侧结构体实例而不接管其内存所有权。例如const m new MyStruct( functionReturningASharedPtr() ); // 调用 m.dispose() 不会释放被包装的 C 侧实例 // 但会触发任何 ondispose 处理器。这个行为的源码依据在 jaccwabyt.js 的__allocStruct中传入指针时对象被标记为(pointer-is-external)__freeStruct释放时若指针是外部的则不调用dealloc()见 jaccwabyt.js 中的if(!obj[xPtrPropName]) dealloc(m)。内存管理的两个内部机制从源码可以确认两个与内存管理相关的关键实现细节指针存放在 WeakMap 中为避免 JS 代码轻易复制到过期的指针副本结构对象到其 WASM 指针的映射被放在工厂闭包内的一个WeakMap__instancePointerMap里指针只能通过只读的pointer访问器属性取得[jaccwabyt.js](https://link.gitcode.com/i/7eea521df24f387544d5b0ab813d7549#L215-L215, L328-L335)。字节序按平台处理代码在初始化时用DataView.setInt16检测当前平台是否为小端所有成员读写都显式传入该字节序标志保证与 C 侧布局一致jaccwabyt.js。Pvsp在成员签名中的区别实验特性签名字母p表示指针在 WASM 中即整数。p在大多数语境下被当作整数处理同时它又是一种独立的类型类比 C 中指针只是无符号数的一种特殊用法。大写P改变普通成员指针截至写作时不含函数指针成员的语义当通过myStruct.x y设置一个P类型成员时如果y instanceof StructType则把y.pointer的值存入myStruct.x如果y既不是数字也不是 StructType则触发异常无论用p还是P都一样。在 jaccwabyt.js 的 setter 实现中可以找到对应逻辑isAutoPtrSig(descr.signature)即P时若被赋的值是StructType实例则取其pointer存入null会被特判为 0。成员签名体系Signature成员签名描述成员的数据类型是 EmscriptenaddFunction()所用格式的扩展变体。非函数指针成员或要被建模为不透明指针的函数指针成员的签名是单个字母函数指针的签名也可以是描述调用签名的一串字母。支持的字母如下vvoid仅用作函数指针成员的返回类型iint324 字节jint648 字节仅当代码启用 BigInt 支持时才真正可用例如 Emscripten 的-sWASM_BIGINT构建标志。否则本 API 遇到j签名条目时可能抛出异常。ffloat4 字节ddouble8 字节cint81 字节char——注意下文说明Cuint81 字节无符号 char——注意下文说明pint32见下文说明P 同p但有额外处理见上文。s 类似int32但它是一个提示该成员是指向字符串的指针某些非常有限的上下文可以按字符串处理。注意这类算法在缺乏相反信息时必须假定编码是 UTF-8、且该指针成员以 NUL 结尾。如果你的字符串成员不是这种情况不要用s改用i或p自行处理字符串。需要特别指出所有这些类型都是数值型。除明确注明的例外情况外给任何结构绑定属性赋非数值都会触发异常。在 [jaccwabyt.js](https://link.gitcode.com/i/7eea521df24f387544d5b0ab813d7549#L521, L594-L601) 中setter 用isNumericValueNumber.isFinite或 BigInt校验非法值会toss。Char 类型WASM 没有定义int8类型也不区分有符号与无符号。本 API 在使用DataView取值/设值时把c当作int8、C当作uint8。不建议在新 WASM 代码中使用这两种类型它们是为了把一些不可变的遗留代码绑定到 WASM 而加入的。旁注Emscripten 的公开文档没有提到p但其生成代码把p作为i的别名大概意为pointer。虽然i对指针类型签名合法但p更具描述性因此本框架鼓励对指针类型成员使用p。用p还有助于为WASM 将来支持 64 位指针做前瞻。注意有时p实际是指向指针的指针但 Emscripten 的 JS/WASM glue 在这种签名里不提供该级别的表达力我们需要在 JS 代码里自行留意何时要处理指针与指向指针的指针。花絮本 API 在某些上下文中把p与i明确区别对待因此鼓励对指针类型使用p。函数指针签名形如x(...)的签名表示函数指针成员x表示非函数成员。无参函数使用x()形式。函数类型签名可以这样构造去掉(和)字符后直接传给 Emscripten 的addFunction()。为了与 Emscripten 公开文档一致还应把p、c、C替换成i。在 JavaScript 中大致是signature.replace(/[^vipPsjfdcC]/g,).replace(/[pPscC]/g,i);该转换逻辑在 jaccwabyt.js 的__memberSignature中也有对应实现memberSignature(memberName, true)的第二个参数即请求 Emscripten 格式。API 参考APIBinder FactoryStructBinderFactory这是整个 API 最顶层的函数所有其他函数与类型都由它生成。其签名是Function StructBinderFactory(object configOptions);它返回一个函数即下文提到的 StructBinder出错时抛出异常。配置对象支持以下选项heap必须是代表 WASM 堆内存的WebAssembly.Memory实例或返回 WASM 堆的 Int8Array/Uint8Array 视图的函数。后者应在环境允许时考虑到堆可能增长。Jaccwabyt 以应该兼容堆运行时增长的方式使用该属性但该场景未经测试。alloc必须是与 EmscriptenModule._malloc()语义兼容的函数——接收要分配的字节数返回指针。分配失败时可以返回 0 或抛出异常。本 API 在分配失败时抛出异常或传播分配器抛出的异常。分配器必须与heap配置项使用同一个堆。dealloc必须是与 EmscriptenModule._free()语义兼容的函数——接收alloc()返回的指针并释放内存。它绝不能抛异常且必须接受 0/null 表示什么都不做注意 0 在 WASM 中技术上是合法的内存地址但作者认为那像是个设计缺陷。bigIntEnabledbool若BigInt64Array可用则默认为 true否则 false为 true 时与其配合使用的 WASM 代码必须已用 int64 支持编译如 Emscripten 的-sWASM_BIGINT标志。若不满足应把该标志设为 false。启用后假定 BigInt 支持可用并开启某些额外特性。在禁用状态下尝试使用需要 BigInt 的特性如 64 位整数类型会触发异常。源码中默认值逻辑见 jaccwabyt.js。memberPrefix/memberSuffixstring若设置结构定义属性会以此字符串作为 JS 侧绑定属性的前缀/后缀。可用于避免结构成员与 JS 侧符号名冲突或让对象级属性更明确地分出属于结构映射与属于 JS 侧。它不修改结构描述对象中的值只影响通过属性访问操作访问时的属性名StructInstance 的各种 API 通常同时接受原始名与加前后缀后的名字。SQLite WASM 集成代码就设置了memberPrefix: $并注释永远不要修改此前缀已固化进大量代码与面向客户端的文档。log用于调试输出的可选函数。默认使用console.log但默认不产生任何调试输出。本 API 假定该函数像console.log一样以空格分隔各参数。参见附录 D 了解如何启用调试输出。APIStruct BinderStruct Binder 是由 StructBinderFactory 创建的工厂。一个 Struct Binder 可以处理任意数量的不同结构体。典型设置中应用只有一个共享的 Binder Factory 和一个 Struct Binder。通过不同 StructBinderFactory 调用创建的 Struct Binder 互不相关除间接经由工厂配置如内存堆外不共享状态。这些工厂有两种调用签名Function StructBinder([string structName,] object structDescription)若结构描述参数带name属性则名称参数可选否则必填。返回值是所描述结构体实例的构造器每个都派生自独立的 StructType 实例。Struct Binder 具有以下成员allocCString(str)分配一份新的、UTF-8 编码、NUL 结尾的给定 JS 字符串副本返回其相对于config.heap()的地址。若分配返回 0 则抛出异常。内存所有权转移给调用者调用者最终必须把它交给配置的config.dealloc()。实现见 jaccwabyt.js。config传给 StructBinderFactory 的配置对象主要用于访问内存释放分配器与内存。修改其中任何重要配置值可能导致未定义行为。APIStruct TypeStructType类是 StructBinder 函数的一个属性。StructBinder 创建的每个构造器都继承自它自己的StructType 类实例后者包含该结构类型特有的状态如结构名与描述元数据。通过不同 StructBinder 实例创建的 StructType 互不相关除工厂配置选项外不共享状态。StructType 构造器不能由客户端代码调用只能由 StructBinder 生成的构造器调用。StructBinder.StructType对象具有以下静态属性各实例可通过theInstance.constructor访问addOnDispose(...value)若对象没有ondispose属性则创建数组并把给定值 push 进去若对象有函数类型的ondispose则将其替换为数组并把该函数移入数组其他情况假定ondispose是数组并追加参数。返回this。allocCString(str)与 StructBinder 的同名方法相同。hasExternalPointer(object)若给定对象的pointer成员指向外部对象则返回 true。即指针被传给结构构造器的情况。若为 true内存由对象之外的人所有且必须活得比该对象久。isA(value)若其参数是与本 StructType来自同一 StructBinder的 StructType 实例则返回 true。memberKey(string)返回被配置的memberPrefix/memberSuffix包装后的字符串。例如传x且memberPrefix为$则返回$x。它不校验该属性是否为结构成员只做字符串变换。基础 StructType 原型具有以下成员全部被结构实例继承除非特别注明否则只能合法地作用于具体结构实例dispose()若合适则释放构造器分配的 WASM 内存。若在 JS 引擎清理对象之前未调用将导致 WASM 堆内存池泄漏。调用dispose()时若对象有名为ondispose的属性则按如下规则处理若是函数以结构对象作为this调用。该方法绝不能抛异常——若抛出异常会被忽略。若是数组可包含函数、指针、其他 StructType 实例和/或 JS 字符串。若条目是函数按上文方式调用若是数字假定为指针并传给父 StructBinder 配置的dealloc()若是 StructType 实例则调用其dispose()若是 JS 字符串假定为对列表下一项的有用描述直接忽略字符串主要作为调试信息支持。某些结构 API 会操作ondispose成员需要时创建为数组或从函数转换为数组。lookupMember(memberName, throwIfNotFoundtrue)给定映射结构成员名返回成员描述对象。找不到时要么抛出第二个参数为 true要么返回undefined第二个参数为 false。第一个参数可以是结构描述中映射的成员名也可以是应用了memberPrefix/memberSuffix后的名字前者查找更快。该方法可以直接在原型上调用无需结构实例。memberToJsString(memberName)用this.lookupMember(memberName, true)查找成员。若签名是s则假定它指向 NUL 结尾、UTF-8 编码的字符串并解码其内存若签名不是s则抛异常。若地址为 0返回null。参见setMemberCString()。其实现jaccwabyt.js会逐字节扫描堆内存直到 NUL 终止符再用TextDecoder解码。memberIsString(memberName [,throwIfNotFoundtrue])用this.lookupMember(memberName, throwIfNotFound)查找成员。若成员签名是s则返回成员描述对象否则返回 false。若给定成员找不到第二个参数为 true 时抛出否则返回 false。memberKey(string)与StructBinder.StructType.memberKey()完全相同。memberKeys()返回此对象上指向 C 侧结构对应物的属性名数组。memberSignature(memberName [,emscriptenFormatfalse])返回给定成员属性的签名使用本框架格式或第二个参数为真值时适合作为 EmscriptenaddFunction()第二参数的格式。若第一个参数不能解析为结构绑定成员名则抛出异常。memoryDump()返回一个Uint8Array包含该对象原始内存缓冲区的当前状态。对调试可能有用但也仅此而已。注意为与 C 兼容内存必然按宿主平台字节序写入因此不适合作为持久化/可移植的序列化格式。setMemberCString(memberName, str)用StructType.allocCString()分配新的 C 风格字符串赋给给定成员并把新字符串加入该对象的ondispose列表以便this.dispose()时清理。若lookupMember()对给定成员名失败、字符串分配失败、或成员签名不是s则抛出异常。返回this。注意重复调用不会立即释放之前的值因为本代码无法知道它们是否在别处即 C 侧仍被使用。相反每次调用时之前的值都会保留在ondispose列表中待结构体销毁时一并清理。由于此类组合中内存所有权与生命周期的复杂性建议尽量减少从 JS 使用 C 字符串成员或让关系单向化让 C 管理字符串JS 只用例如memberToJsString()获取。APIStruct Constructors结构构造器StructBinder 返回的函数自然用于创建给定结构类型的新实例const x new MyStruct;正常情况下不应传参数但可选地接受单个参数一个 WASM 堆指针地址该对象将用它作为存储。它不接管该内存的所有权且该内存在此结构实例存活期间必须有效。这用于例如代理静态/共享的 C 侧实例const x new MyStruct( someCFuncWhichReturnsAMyStructPointer() ); ... x.dispose(); // 不释放内存这种情况下 JS 侧对象不拥有内存也无法知道 C 侧结构何时被销毁。若 JS 侧结构在 C 侧结构的成员被释放后继续使用结果是明确未定义的。潜在 TODO增加一种把 C 侧结构所有权传给 JS 侧对象的方式。例如也许传true作为第二个参数告诉构造器接管所有权。目前可以在创建后立即用类似myStruct.ondispose[myStruct.pointer]的方式接管指针。这些构造器具有以下静态成员isA(value)若参数由该构造器创建则返回 true。memberKey(string)与 StructType 中文档一致。memberKeys(string)与 StructType 中文档一致。structInfo生成该构造器时传给 StructBinder 的结构描述。structName生成该构造器时传给 StructBinder 的结构名。APIStruct Prototypes通过上述构造器创建的结构体其原型是各结构类型特有的 StructType 实例并向混入中增加以下结构类型特有的属性structInfo创建该类的 StructBinder 所收到的结构描述元数据。structName创建该类的 StructBinder 所收到的结构名。APIStruct Instances通过上述构造器创建的结构体实例都具有以下公共的实例特有状态pointer只读数值属性即构造该对象时配置的分配器返回的指针。调用dispose()继承自 StructType后此属性值为undefined。当调用接收此类结构指针的 C 侧代码时直接传myStruct.pointer即可。在 libSQL/SQLite WASM 集成中的实际用法Jaccwabyt 并非孤立存在它是 SQLite WASM API 基础设施的一部分。在 tester1.c-pp.js 的测试中展示了完整而真实的使用模式可作为最佳实践范本const MyStructDef { sizeof: 16, members: { p4: {offset: 0, sizeof: 4, signature: i}, pP: {offset: 4, sizeof: 4, signature: P}, ro: {offset: 8, sizeof: 4, signature: i, readOnly: true}, cstr:{offset: 12, sizeof: 4, signature: s} } }; // 启用 BigInt 时追加 64 位成员 if (W.bigIntEnabled) { MyStructDef.members.p8 {offset: MyStructDef.sizeof, sizeof: 8, signature: j}; MyStructDef.sizeof 8; } const StructType S.StructBinder.StructType; const K S.StructBinder(my_struct, MyStructDef); const k1 new K(); try { // 成员读写经由内存编组而不是普通 JS 属性 k1.$p4 1; k1.$pP 2; // P 类型成员可以赋另一个结构实例取其 pointer k1.$pP k2; // null 被特判为 0 k1.$pP null; // C 字符串分配、赋值并自动进入 ondispose k1.setMemberCString(cstr, A C-string.); // 只读成员赋值会抛异常 // k1.$ro 1; // throws // 指针只读且 dispose 后为 undefined let ptr k1.pointer; k1.dispose(); } finally { k1.dispose(); k2.dispose(); }测试中还验证了 C 侧函数通过testFunc(wts.pointer)修改结构体后JS 侧读取成员值能立刻看到变化例如wts.$v8从20n变为40n、80n这正是双向可见的实证。此外wts.$xFunc W.installFunction(wtsFunc, wts.memberSignature(xFunc))演示了把 JS 函数绑定为 C 可调用的函数指针成员。附录 A限制、TODO 与非 TODO本库只支持 WASM 支持的基本成员类型集合数字包含指针。嵌套结构体不处理除非一个成员是指向这类结构体的指针。是否支持嵌套完全取决于开发者将来是否需要。JS 与 C 之间的字符串转换需要各 WASM 环境特有的基础设施本库不直接支持。把函数绑定到结构实例上、使 C 能看到并调用 JS 定义函数这件事并不像它本可以的那样透明原因是 EmscriptenaddFunction()/removeFunction()接口存在缺陷。在该 API 的替代方案出现之前此支持相当有限。确实可以把 JS 定义的函数绑定到 C 侧函数指针并从 C 调用缺少的只是更易用/更透明的支持。与此同时Jaccwabyt 的一个独立子项目whwasmutil.js见 ext/wasm/common/whwasmutil.js提供了这样的绑定机制但把它直接集成进 Jaccwabyt 不仅会让库体积翻倍还不止也感觉不太合适因此如何通过完全可选的 StructBinderFactory 配置项来提供该能力还需要实验。也许值得考虑把 C 绑定成员的访问移进子对象例如 JS 侧通过myStructInstance.s.structMember访问主要好处是消除哪些成员属于 C 结构、哪些纯属 JS的混淆但问题在于需要内部把s成员映射回包含它的对象成本更高、又多了一个可能出错的部件。也许值得考虑提供反序列化支持。它会非常有限例如无法有意义地序列化任意指针但对只含数值或 C 字符串状态的结构体可能有用。目前的实现中客户端代码很容易为各自应用写出合适的包装。本库若提供任何实现都有个缺点可能无意中序列化指针因为它们只是整数反序列化后造成潜在混乱。也许可以扩展结构描述把特定成员标记为可序列化并说明如何序列化。附录 D调试信息StructBinderFactory、StructBinder 与 StructType 类都有以下不受支持的方法主要为了辅助它们自身的开发而非供客户端代码使用debugFlags(flags)整数一个不受支持的调试选项可随时更改或移除。参数是一组标志位用于启用/禁用属性访问器的某些调试/跟踪输出0x01用于 getter、0x02用于 setter、0x04用于分配allocations、0x08用于释放deallocations。传 0 禁用所有标志传负值完全清除所有标志后者还有副作用告诉标志从层级中下一个更高级的类继承最上层是 StructBinderFactory其次是 StructBinder然后是 StructType。标志解析实现见 jaccwabyt.js。附录 G从 C 生成结构描述结构定义理想情况下应从 WASM 编译的 C 中生成而不是靠猜测 sizeof 和 offset——这样可以用 C 的sizeof()与offsetof()收集大小与偏移信息注意结构填充可能以不那么直观的方式影响偏移手写绝对不推荐。具体如何生成描述必然因项目而异。作者提醒哦这很简单我们手写就行是愚蠢的想法——结构大小与字节偏移必须与 C 侧代码看到的一致否则运行时结果完全未定义。开发与测试本软件所用的方法是用一小套宏从 C99 及以上代码生成静态字符串内存中的结构描述。把这样的文件加入你的 WASM 构建安排其函数被导出Emscripten 中把函数名加_前缀加入项目的EXPORT_FUNCTIONS列表然后从 JS 调用它注意需要环境特定的 JS glue 把返回指针转换成 JS 侧字符串用JSON.parse()处理再把包含的结构描述喂给绑定工厂。下面是一个完整可复制粘贴的示例#include string.h /* memset() */ #include stddef.h /* offsetof() */ #include stdio.h /* snprintf() */ #include stdint.h /* int64_t */ #include assert.h struct ExampleStruct { int v4; void * ppV; int64_t v8; void (*xFunc)(void*); }; typedef struct ExampleStruct ExampleStruct; const char * wasm__ctype_json(void){ static char strBuf[512 * 8] {0} /* 静态缓冲区必须足够容纳我们的 JSON。 字符串生成宏会尽力在缓冲区过小时 assert()。 */; int n 0, structCount 0 /* 供宏使用的计数器 */; char * pos strBuf[1] /* 写位置游标。先跳过第一个字节以帮助 防止一个小的竞态条件 */; char const * const zEnd pos sizeof(strBuf) /* 超过末尾的游标虚拟 EOF */; if(strBuf[0]) return strBuf; // 之前在调用中已设置好。 //////////////////////////////////////////////////////////////////// // 首先构建我们的宏框架... //////////////////////////////////////////////////////////////////// // 核心输出生成宏... #define lenCheck assert(pos zEnd - 100) #define outf(format,...) \ pos snprintf(pos, ((size_t)(zEnd - pos)), format, __VA_ARGS__); \ lenCheck #define out(TXT) outf(%s,TXT) #define CloseBrace(LEVEL) \ assert(LEVEL5); memset(pos, }, LEVEL); posLEVEL; lenCheck //////////////////////////////////////////////////////////////////// // 生成 StructBinders 的宏... #define StructBinder__(TYPE) \ n 0; \ outf(%s{, (structCount ? , : )); \ out(\name\: \ # TYPE \,); \ outf(\sizeof\: %d, (int)sizeof(TYPE)); \ out(,\members\: {); #define StructBinder_(T) StructBinder__(T) // ^^^ 需要额外一层间接以展开 CurrentStruct #define StructBinder StructBinder_(CurrentStruct) #define _StructBinder CloseBrace(2) #define M(MEMBER,SIG) \ outf(%s\%s\: \ {\offset\:%d,\sizeof\: %d,\signature\:\%s\}, \ (n ? , : ), #MEMBER, \ (int)offsetof(CurrentStruct,MEMBER), \ (int)sizeof(((CurrentStruct*)0)-MEMBER), \ SIG) // 宏结束。 //////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////// // 有了这些就可以做正事了。 out(\structs\: [); { // 对每个结构描述执行... #define CurrentStruct ExampleStruct StructBinder { M(v4,i); M(ppV,p); M(v8,j); M(xFunc,v(p)); } _StructBinder; #undef CurrentStruct } out( ]/*structs*/); //////////////////////////////////////////////////////////////////// // 完成收尾输出... out(}/*top-level wrapper*/); *pos 0; strBuf[0] {/*竞态条件规避的收尾*/; return strBuf; // 如果这个文件将来会与其他文件拼接或被 #include // 清理我们的宏是良好的实践 #undef StructBinder #undef StructBinder_ #undef StructBinder__ #undef M #undef _StructBinder #undef CloseBrace #undef out #undef outf #undef lenCheck }这段 C 代码会生成形如{structs:[{name:ExampleStruct,sizeof:24,members:{...}}]}的 JSON 字符串其中xFunc的签名v(p)会被memberSignature(xFunc, true)或等效转换规约为 Emscripten 可用的vi。快速参考关键文件库文档libsql-sqlite3/ext/wasm/jaccwabyt/jaccwabyt.md单文件实现libsql-sqlite3/ext/wasm/jaccwabyt/jaccwabyt.jsSQLite WASM 集成示例libsql-sqlite3/ext/wasm/api/sqlite3-api-glue.c-pp.js构建链说明libsql-sqlite3/ext/wasm/api/README.md真实测试用例libsql-sqlite3/ext/wasm/tester1.c-pp.js含StructBinder结构绑定、C 字符串、P类型指针、BigInt 与函数指针成员的双向读写验证【免费下载链接】libsqllibSQL is a fork of SQLite that is both Open Source, and Open Contributions.项目地址: https://gitcode.com/GitHub_Trending/li/libsql创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考