4 The author disclaims copyright to this source code. In place of a
5 legal notice, here is a blessing:
7 * May you do good and not evil.
8 * May you find forgiveness for yourself and forgive others.
9 * May you share freely, never taking more than you give.
11 ***********************************************************************
13 The Jaccwabyt API is documented in detail in an external file,
14 _possibly_ called jaccwabyt.md in the same directory as this file.
17 - https://fossil.wanderinghorse.net/r/jaccwabyt
18 - https://sqlite.org/src/dir/ext/wasm/jaccwabyt
22 globalThis.Jaccwabyt = function StructBinderFactory(config){
23 /* ^^^^ it is recommended that clients move that object into wherever
24 they'd like to have it and delete the self-held copy ("self" being
25 the global window or worker object). This API does not require the
26 global reference - it is simply installed as a convenience for
27 connecting these bits to other co-developed code before it gets
28 removed from the global namespace.
31 /** Throws a new Error, the message of which is the concatenation
32 all args with a space between each. */
33 const toss = (...args)=>{throw new Error(args.join(' '))};
36 Implementing function bindings revealed significant
37 shortcomings in Emscripten's addFunction()/removeFunction()
40 https://github.com/emscripten-core/emscripten/issues/17323
42 Until those are resolved, or a suitable replacement can be
43 implemented, our function-binding API will be more limited
44 and/or clumsier to use than initially hoped.
46 if(!(config.heap instanceof WebAssembly.Memory)
47 && !(config.heap instanceof Function)){
48 toss("config.heap must be WebAssembly.Memory instance or a function.");
50 ['alloc','dealloc'].forEach(function(k){
51 (config[k] instanceof Function) ||
52 toss("Config option '"+k+"' must be a function.");
54 const SBF = StructBinderFactory;
55 const heap = (config.heap instanceof Function)
56 ? config.heap : (()=>new Uint8Array(config.heap.buffer)),
58 dealloc = config.dealloc,
59 log = config.log || console.log.bind(console),
60 memberPrefix = (config.memberPrefix || ""),
61 memberSuffix = (config.memberSuffix || ""),
62 bigIntEnabled = (undefined===config.bigIntEnabled
63 ? !!globalThis['BigInt64Array'] : !!config.bigIntEnabled),
64 BigInt = globalThis['BigInt'],
65 BigInt64Array = globalThis['BigInt64Array'],
66 /* Undocumented (on purpose) config options: */
67 ptrSizeof = config.ptrSizeof || 4,
68 ptrIR = config.ptrIR || 'i32'
72 SBF.__makeDebugFlags = function(deriveFrom=null){
73 /* This is disgustingly overengineered. :/ */
74 if(deriveFrom && deriveFrom.__flags) deriveFrom = deriveFrom.__flags;
75 const f = function f(flags){
76 if(0===arguments.length){
80 delete f.__flags.getter; delete f.__flags.setter;
81 delete f.__flags.alloc; delete f.__flags.dealloc;
83 f.__flags.getter = 0!==(0x01 & flags);
84 f.__flags.setter = 0!==(0x02 & flags);
85 f.__flags.alloc = 0!==(0x04 & flags);
86 f.__flags.dealloc = 0!==(0x08 & flags);
90 Object.defineProperty(f,'__flags', {
91 iterable: false, writable: false,
92 value: Object.create(deriveFrom)
97 SBF.debugFlags = SBF.__makeDebugFlags();
100 const isLittleEndian = (function() {
101 const buffer = new ArrayBuffer(2);
102 new DataView(buffer).setInt16(0, 256, true /* littleEndian */);
103 // Int16Array uses the platform's endianness.
104 return new Int16Array(buffer)[0] === 256;
107 Some terms used in the internal docs:
109 StructType: a struct-wrapping class generated by this
111 DEF: struct description object.
112 SIG: struct member signature string.
115 /** True if SIG s looks like a function signature, else
117 const isFuncSig = (s)=>'('===s[1];
118 /** True if SIG s is-a pointer signature. */
119 const isPtrSig = (s)=>'p'===s || 'P'===s;
120 const isAutoPtrSig = (s)=>'P'===s /*EXPERIMENTAL*/;
121 const sigLetter = (s)=>isFuncSig(s) ? 'p' : s[0];
122 /** Returns the WASM IR form of the Emscripten-conventional letter
123 at SIG s[0]. Throws for an unknown SIG. */
124 const sigIR = function(s){
125 switch(sigLetter(s)){
126 case 'c': case 'C': return 'i8';
127 case 'i': return 'i32';
128 case 'p': case 'P': case 's': return ptrIR;
129 case 'j': return 'i64';
130 case 'f': return 'float';
131 case 'd': return 'double';
133 toss("Unhandled signature IR:",s);
136 const affirmBigIntArray = BigInt64Array
137 ? ()=>true : ()=>toss('BigInt64Array is not available.');
138 /** Returns the name of a DataView getter method corresponding
140 const sigDVGetter = function(s){
141 switch(sigLetter(s)) {
142 case 'p': case 'P': case 's': {
144 case 4: return 'getInt32';
145 case 8: return affirmBigIntArray() && 'getBigInt64';
149 case 'i': return 'getInt32';
150 case 'c': return 'getInt8';
151 case 'C': return 'getUint8';
152 case 'j': return affirmBigIntArray() && 'getBigInt64';
153 case 'f': return 'getFloat32';
154 case 'd': return 'getFloat64';
156 toss("Unhandled DataView getter for signature:",s);
158 /** Returns the name of a DataView setter method corresponding
160 const sigDVSetter = function(s){
161 switch(sigLetter(s)){
162 case 'p': case 'P': case 's': {
164 case 4: return 'setInt32';
165 case 8: return affirmBigIntArray() && 'setBigInt64';
169 case 'i': return 'setInt32';
170 case 'c': return 'setInt8';
171 case 'C': return 'setUint8';
172 case 'j': return affirmBigIntArray() && 'setBigInt64';
173 case 'f': return 'setFloat32';
174 case 'd': return 'setFloat64';
176 toss("Unhandled DataView setter for signature:",s);
179 Returns either Number of BigInt, depending on the given
180 SIG. This constructor is used in property setters to coerce
181 the being-set value to the correct size.
183 const sigDVSetWrapper = function(s){
184 switch(sigLetter(s)) {
185 case 'i': case 'f': case 'c': case 'C': case 'd': return Number;
186 case 'j': return affirmBigIntArray() && BigInt;
187 case 'p': case 'P': case 's':
189 case 4: return Number;
190 case 8: return affirmBigIntArray() && BigInt;
194 toss("Unhandled DataView set wrapper for signature:",s);
197 /** Returns the given struct and member name in a form suitable for
198 debugging and error output. */
199 const sPropName = (s,k)=>s+'::'+k;
201 const __propThrowOnSet = function(structName,propName){
202 return ()=>toss(sPropName(structName,propName),"is read-only.");
206 In order to completely hide StructBinder-bound struct
207 pointers from JS code, we store them in a scope-local
208 WeakMap which maps the struct-bound objects to their WASM
209 pointers. The pointers are accessible via
210 boundObject.pointer, which is gated behind an accessor
211 function, but are not exposed anywhere else in the
212 object. The main intention of that is to make it impossible
213 for stale copies to be made.
215 const __instancePointerMap = new WeakMap();
217 /** Property name for the pointer-is-external marker. */
218 const xPtrPropName = '(pointer-is-external)';
220 /** Frees the obj.pointer memory and clears the pointer
222 const __freeStruct = function(ctor, obj, m){
223 if(!m) m = __instancePointerMap.get(obj);
225 __instancePointerMap.delete(obj);
226 if(Array.isArray(obj.ondispose)){
228 while((x = obj.ondispose.shift())){
230 if(x instanceof Function) x.call(obj);
231 else if(x instanceof StructType) x.dispose();
232 else if('number' === typeof x) dealloc(x);
233 // else ignore. Strings are permitted to annotate entries
234 // to assist in debugging.
236 console.warn("ondispose() for",ctor.structName,'@',
237 m,'threw. NOT propagating it.',e);
240 }else if(obj.ondispose instanceof Function){
243 /*do not rethrow: destructors must not throw*/
244 console.warn("ondispose() for",ctor.structName,'@',
245 m,'threw. NOT propagating it.',e);
248 delete obj.ondispose;
249 if(ctor.debugFlags.__flags.dealloc){
250 log("debug.dealloc:",(obj[xPtrPropName]?"EXTERNAL":""),
251 ctor.structName,"instance:",
252 ctor.structInfo.sizeof,"bytes @"+m);
254 if(!obj[xPtrPropName]) dealloc(m);
258 /** Returns a skeleton for a read-only property accessor wrapping
260 const rop = (v)=>{return {configurable: false, writable: false,
261 iterable: false, value: v}};
263 /** Allocates obj's memory buffer based on the size defined in
264 ctor.structInfo.sizeof. */
265 const __allocStruct = function(ctor, obj, m){
267 if(m) Object.defineProperty(obj, xPtrPropName, rop(m));
269 m = alloc(ctor.structInfo.sizeof);
270 if(!m) toss("Allocation of",ctor.structName,"structure failed.");
273 if(ctor.debugFlags.__flags.alloc){
274 log("debug.alloc:",(fill?"":"EXTERNAL"),
275 ctor.structName,"instance:",
276 ctor.structInfo.sizeof,"bytes @"+m);
278 if(fill) heap().fill(0, m, m + ctor.structInfo.sizeof);
279 __instancePointerMap.set(obj, m);
281 __freeStruct(ctor, obj, m);
285 /** Gets installed as the memoryDump() method of all structs. */
286 const __memoryDump = function(){
287 const p = this.pointer;
289 ? new Uint8Array(heap().slice(p, p+this.structInfo.sizeof))
293 const __memberKey = (k)=>memberPrefix + k + memberSuffix;
294 const __memberKeyProp = rop(__memberKey);
297 Looks up a struct member in structInfo.members. Throws if found
298 if tossIfNotFound is true, else returns undefined if not
299 found. The given name may be either the name of the
300 structInfo.members key (faster) or the key as modified by the
301 memberPrefix and memberSuffix settings.
303 const __lookupMember = function(structInfo, memberName, tossIfNotFound=true){
304 let m = structInfo.members[memberName];
305 if(!m && (memberPrefix || memberSuffix)){
306 // Check for a match on members[X].key
307 for(const v of Object.values(structInfo.members)){
308 if(v.key===memberName){ m = v; break; }
310 if(!m && tossIfNotFound){
311 toss(sPropName(structInfo.name,memberName),'is not a mapped struct member.');
318 Uses __lookupMember(obj.structInfo,memberName) to find a member,
319 throwing if not found. Returns its signature, either in this
320 framework's native format or in Emscripten format.
322 const __memberSignature = function f(obj,memberName,emscriptenFormat=false){
323 if(!f._) f._ = (x)=>x.replace(/[^vipPsjrdcC]/g,"").replace(/[pPscC]/g,'i');
324 const m = __lookupMember(obj.structInfo, memberName, true);
325 return emscriptenFormat ? f._(m.signature) : m.signature;
328 const __ptrPropDescriptor = {
329 configurable: false, enumerable: false,
330 get: function(){return __instancePointerMap.get(this)},
331 set: ()=>toss("Cannot assign the 'pointer' property of a struct.")
332 // Reminder: leaving `set` undefined makes assignments
333 // to the property _silently_ do nothing. Current unit tests
334 // rely on it throwing, though.
337 /** Impl of X.memberKeys() for StructType and struct ctors. */
338 const __structMemberKeys = rop(function(){
340 for(const k of Object.keys(this.structInfo.members)){
341 a.push(this.memberKey(k));
346 const __utf8Decoder = new TextDecoder('utf-8');
347 const __utf8Encoder = new TextEncoder();
348 /** Internal helper to use in operations which need to distinguish
349 between SharedArrayBuffer heap memory and non-shared heap. */
350 const __SAB = ('undefined'===typeof SharedArrayBuffer)
351 ? function(){} : SharedArrayBuffer;
352 const __utf8Decode = function(arrayBuffer, begin, end){
353 return __utf8Decoder.decode(
354 (arrayBuffer.buffer instanceof __SAB)
355 ? arrayBuffer.slice(begin, end)
356 : arrayBuffer.subarray(begin, end)
360 Uses __lookupMember() to find the given obj.structInfo key.
361 Returns that member if it is a string, else returns false. If the
362 member is not found, throws if tossIfNotFound is true, else
365 const __memberIsString = function(obj,memberName, tossIfNotFound=false){
366 const m = __lookupMember(obj.structInfo, memberName, tossIfNotFound);
367 return (m && 1===m.signature.length && 's'===m.signature[0]) ? m : false;
371 Given a member description object, throws if member.signature is
372 not valid for assigning to or interpretation as a C-style string.
373 It optimistically assumes that any signature of (i,p,s) is
376 const __affirmCStringSignature = function(member){
377 if('s'===member.signature) return;
378 toss("Invalid member type signature for C-string value:",
379 JSON.stringify(member));
383 Looks up the given member in obj.structInfo. If it has a
384 signature of 's' then it is assumed to be a C-style UTF-8 string
385 and a decoded copy of the string at its address is returned. If
386 the signature is of any other type, it throws. If an s-type
387 member's address is 0, `null` is returned.
389 const __memberToJsString = function f(obj,memberName){
390 const m = __lookupMember(obj.structInfo, memberName, true);
391 __affirmCStringSignature(m);
392 const addr = obj[m.key];
393 //log("addr =",addr,memberName,"m =",m);
394 if(!addr) return null;
397 for( ; mem[pos]!==0; ++pos ) {
398 //log("mem[",pos,"]",mem[pos]);
400 //log("addr =",addr,"pos =",pos);
401 return (addr===pos) ? "" : __utf8Decode(mem, addr, pos);
405 Adds value v to obj.ondispose, creating ondispose,
406 or converting it to an array, if needed.
408 const __addOnDispose = function(obj, ...v){
410 if(!Array.isArray(obj.ondispose)){
411 obj.ondispose = [obj.ondispose];
416 obj.ondispose.push(...v);
420 Allocates a new UTF-8-encoded, NUL-terminated copy of the given
421 JS string and returns its address relative to heap(). If
422 allocation returns 0 this function throws. Ownership of the
423 memory is transfered to the caller, who must eventually pass it
424 to the configured dealloc() function.
426 const __allocCString = function(str){
427 const u = __utf8Encoder.encode(str);
428 const mem = alloc(u.length+1);
429 if(!mem) toss("Allocation error while duplicating string:",str);
432 //for( ; i < u.length; ++i ) h[mem + i] = u[i];
434 h[mem + u.length] = 0;
435 //log("allocCString @",mem," =",u);
440 Sets the given struct member of obj to a dynamically-allocated,
441 UTF-8-encoded, NUL-terminated copy of str. It is up to the caller
442 to free any prior memory, if appropriate. The newly-allocated
443 string is added to obj.ondispose so will be freed when the object
446 The given name may be either the name of the structInfo.members
447 key (faster) or the key as modified by the memberPrefix and
448 memberSuffix settings.
450 const __setMemberCString = function(obj, memberName, str){
451 const m = __lookupMember(obj.structInfo, memberName, true);
452 __affirmCStringSignature(m);
453 /* Potential TODO: if obj.ondispose contains obj[m.key] then
454 dealloc that value and clear that ondispose entry */
455 const mem = __allocCString(str);
457 __addOnDispose(obj, mem);
462 Prototype for all StructFactory instances (the constructors
463 returned from StructBinder).
465 const StructType = function ctor(structName, structInfo){
466 if(arguments[2]!==rop){
467 toss("Do not call the StructType constructor",
468 "from client-level code.");
470 Object.defineProperties(this,{
471 //isA: rop((v)=>v instanceof ctor),
472 structName: rop(structName),
473 structInfo: rop(structInfo)
478 Properties inherited by struct-type-specific StructType instances
479 and (indirectly) concrete struct-type instances.
481 StructType.prototype = Object.create(null, {
482 dispose: rop(function(){__freeStruct(this.constructor, this)}),
483 lookupMember: rop(function(memberName, tossIfNotFound=true){
484 return __lookupMember(this.structInfo, memberName, tossIfNotFound);
486 memberToJsString: rop(function(memberName){
487 return __memberToJsString(this, memberName);
489 memberIsString: rop(function(memberName, tossIfNotFound=true){
490 return __memberIsString(this, memberName, tossIfNotFound);
492 memberKey: __memberKeyProp,
493 memberKeys: __structMemberKeys,
494 memberSignature: rop(function(memberName, emscriptenFormat=false){
495 return __memberSignature(this, memberName, emscriptenFormat);
497 memoryDump: rop(__memoryDump),
498 pointer: __ptrPropDescriptor,
499 setMemberCString: rop(function(memberName, str){
500 return __setMemberCString(this, memberName, str);
503 // Function-type non-Property inherited members
504 Object.assign(StructType.prototype,{
505 addOnDispose: function(...v){
506 __addOnDispose(this,...v);
512 "Static" properties for StructType.
514 Object.defineProperties(StructType, {
515 allocCString: rop(__allocCString),
516 isA: rop((v)=>v instanceof StructType),
517 hasExternalPointer: rop((v)=>(v instanceof StructType) && !!v[xPtrPropName]),
518 memberKey: __memberKeyProp
521 const isNumericValue = (v)=>Number.isFinite(v) || (v instanceof (BigInt || Number));
524 Pass this a StructBinder-generated prototype, and the struct
525 member description object. It will define property accessors for
526 proto[memberKey] which read from/write to memory in
527 this.pointer. It modifies descr to make certain downstream
528 operations much simpler.
530 const makeMemberWrapper = function f(ctor,name, descr){
532 /*cache all available getters/setters/set-wrappers for
533 direct reuse in each accessor function. */
534 f._ = {getters: {}, setters: {}, sw:{}};
535 const a = ['i','c','C','p','P','s','f','d','v()'];
536 if(bigIntEnabled) a.push('j');
537 a.forEach(function(v){
538 //const ir = sigIR(v);
539 f._.getters[v] = sigDVGetter(v) /* DataView[MethodName] values for GETTERS */;
540 f._.setters[v] = sigDVSetter(v) /* DataView[MethodName] values for SETTERS */;
541 f._.sw[v] = sigDVSetWrapper(v) /* BigInt or Number ctor to wrap around values
544 const rxSig1 = /^[ipPsjfdcC]$/,
545 rxSig2 = /^[vipPsjfdcC]\([ipPsjfdcC]*\)$/;
546 f.sigCheck = function(obj, name, key,sig){
547 if(Object.prototype.hasOwnProperty.call(obj, key)){
548 toss(obj.structName,'already has a property named',key+'.');
550 rxSig1.test(sig) || rxSig2.test(sig)
551 || toss("Malformed signature for",
552 sPropName(obj.structName,name)+":",sig);
555 const key = ctor.memberKey(name);
556 f.sigCheck(ctor.prototype, name, key, descr.signature);
559 const sigGlyph = sigLetter(descr.signature);
560 const xPropName = sPropName(ctor.prototype.structName,key);
561 const dbg = ctor.prototype.debugFlags.__flags;
563 TODO?: set prototype of descr to an object which can set/fetch
564 its prefered representation, e.g. conversion to string or mapped
565 function. Advantage: we can avoid doing that via if/else if/else
566 in the get/set methods.
568 const prop = Object.create(null);
569 prop.configurable = false;
570 prop.enumerable = false;
571 prop.get = function(){
573 log("debug.getter:",f._.getters[sigGlyph],"for", sigIR(sigGlyph),
574 xPropName,'@', this.pointer,'+',descr.offset,'sz',descr.sizeof);
577 new DataView(heap().buffer, this.pointer + descr.offset, descr.sizeof)
578 )[f._.getters[sigGlyph]](0, isLittleEndian);
579 if(dbg.getter) log("debug.getter:",xPropName,"result =",rc);
583 prop.set = __propThrowOnSet(ctor.prototype.structName,key);
585 prop.set = function(v){
587 log("debug.setter:",f._.setters[sigGlyph],"for", sigIR(sigGlyph),
588 xPropName,'@', this.pointer,'+',descr.offset,'sz',descr.sizeof, v);
591 toss("Cannot set struct property on disposed instance.");
594 else while(!isNumericValue(v)){
595 if(isAutoPtrSig(descr.signature) && (v instanceof StructType)){
596 // It's a struct instance: let's store its pointer value!
598 if(dbg.setter) log("debug.setter:",xPropName,"resolved to",v);
601 toss("Invalid value for pointer-type",xPropName+'.');
604 new DataView(heap().buffer, this.pointer + descr.offset, descr.sizeof)
605 )[f._.setters[sigGlyph]](0, f._.sw[sigGlyph](v), isLittleEndian);
608 Object.defineProperty(ctor.prototype, key, prop);
609 }/*makeMemberWrapper*/;
612 The main factory function which will be returned to the
615 const StructBinder = function StructBinder(structName, structInfo){
616 if(1===arguments.length){
617 structInfo = structName;
618 structName = structInfo.name;
619 }else if(!structInfo.name){
620 structInfo.name = structName;
622 if(!structName) toss("Struct name is required.");
623 let lastMember = false;
624 Object.keys(structInfo.members).forEach((k)=>{
625 // Sanity checks of sizeof/offset info...
626 const m = structInfo.members[k];
627 if(!m.sizeof) toss(structName,"member",k,"is missing sizeof.");
628 else if(m.sizeof===1){
629 (m.signature === 'c' || m.signature === 'C') ||
630 toss("Unexpected sizeof==1 member",
631 sPropName(structInfo.name,k),
632 "with signature",m.signature);
634 // sizes and offsets of size-1 members may be odd values, but
636 if(0!==(m.sizeof%4)){
637 console.warn("Invalid struct member description =",m,"from",structInfo);
638 toss(structName,"member",k,"sizeof is not aligned. sizeof="+m.sizeof);
640 if(0!==(m.offset%4)){
641 console.warn("Invalid struct member description =",m,"from",structInfo);
642 toss(structName,"member",k,"offset is not aligned. offset="+m.offset);
645 if(!lastMember || lastMember.offset < m.offset) lastMember = m;
647 if(!lastMember) toss("No member property descriptions found.");
648 else if(structInfo.sizeof < lastMember.offset+lastMember.sizeof){
649 toss("Invalid struct config:",structName,
650 "max member offset ("+lastMember.offset+") ",
651 "extends past end of struct (sizeof="+structInfo.sizeof+").");
653 const debugFlags = rop(SBF.__makeDebugFlags(StructBinder.debugFlags));
654 /** Constructor for the StructCtor. */
655 const StructCtor = function StructCtor(externalMemory){
656 if(!(this instanceof StructCtor)){
657 toss("The",structName,"constructor may only be called via 'new'.");
658 }else if(arguments.length){
659 if(externalMemory!==(externalMemory|0) || externalMemory<=0){
660 toss("Invalid pointer value for",structName,"constructor.");
662 __allocStruct(StructCtor, this, externalMemory);
664 __allocStruct(StructCtor, this);
667 Object.defineProperties(StructCtor,{
668 debugFlags: debugFlags,
669 isA: rop((v)=>v instanceof StructCtor),
670 memberKey: __memberKeyProp,
671 memberKeys: __structMemberKeys,
672 methodInfoForKey: rop(function(mKey){
674 structInfo: rop(structInfo),
675 structName: rop(structName)
677 StructCtor.prototype = new StructType(structName, structInfo, rop);
678 Object.defineProperties(StructCtor.prototype,{
679 debugFlags: debugFlags,
680 constructor: rop(StructCtor)
681 /*if we assign StructCtor.prototype and don't do
682 this then StructCtor!==instance.constructor!*/
684 Object.keys(structInfo.members).forEach(
685 (name)=>makeMemberWrapper(StructCtor, name, structInfo.members[name])
689 StructBinder.StructType = StructType;
690 StructBinder.config = config;
691 StructBinder.allocCString = __allocCString;
692 if(!StructBinder.debugFlags){
693 StructBinder.debugFlags = SBF.__makeDebugFlags(SBF.debugFlags);
696 }/*StructBinderFactory*/;