/** 2022-06-30 The author disclaims copyright to this source code. In place of a legal notice, here is a blessing: * 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. *********************************************************************** The Jaccwabyt API is documented in detail in an external file, _possibly_ called jaccwabyt.md in the same directory as this file. Project homes: - https://fossil.wanderinghorse.net/r/jaccwabyt - https://sqlite.org/src/dir/ext/wasm/jaccwabyt */ 'use strict'; globalThis.Jaccwabyt = function StructBinderFactory(config){ /* ^^^^ it is recommended that clients move that object into wherever they'd like to have it and delete the globalThis-held copy. This API does not require the global reference - it is simply installed as a convenience for connecting these bits to other co-developed code before it gets removed from the global namespace. */ /** Throws a new Error, the message of which is the concatenation all args with a space between each. */ const toss = (...args)=>{throw new Error(args.join(' '))}; if(!(config.heap instanceof WebAssembly.Memory) && !(config.heap instanceof Function)){ toss("config.heap must be WebAssembly.Memory instance or a function which returns one."); } ['alloc','dealloc'].forEach(function(k){ (config[k] instanceof Function) || toss("Config option '"+k+"' must be a function."); }); const __heap = config.heap; const SBF = StructBinderFactory; const heap = __heap ? __heap : ()=>new Uint8Array(__heap.buffer), alloc = config.alloc, dealloc = config.dealloc, log = config.log || console.debug.bind(console), memberPrefix = (config.memberPrefix || ""), memberSuffix = (config.memberSuffix || ""), BigInt = globalThis['BigInt'], BigInt64Array = globalThis['BigInt64Array'], bigIntEnabled = config.bigIntEnabled ?? !!BigInt64Array; //console.warn("config",config); let ptr = alloc(1); const ptrIR = config.pointerIR || config.ptrIR/*deprecated*/ || ('bigint'===typeof ptr ? 'i64' : 'i32'); /* Undocumented (on purpose) config options: */ const ptrSize = config.ptrSize/*deprecated*/ || ('i32'===ptrIR ? 4 : 8); dealloc(ptr); ptr = undefined; //console.warn("ptrIR =",ptrIR,"ptrSize =",ptrSize); if(ptrIR!=='i32' && ptrIR!=='i64') toss("Invalid pointer representation:",ptrIR); if(ptrSize!==4 && ptrSize!==8) toss("Invalid pointer size:",ptrSize); /** Either BigInt or, if !bigIntEnabled, a function which throws complaining that BigInt is not enabled. */ const __BigInt = (bigIntEnabled && BigInt) ? (v)=>BigInt(v || 0) : (v)=>toss("BigInt support is disabled in this build."); const __asPtrType = ('i32'==ptrIR) ? Number : __BigInt; const __NullPtr = __asPtrType(0); /** Expects any number of numeric arguments, each one of either type Number or BigInt. It sums them up (from an implicit starting point of 0 or 0n) and returns them as a number of the same type which target.asPtrType() uses. This is a workaround for not being able to mix Number/BigInt in addition/subtraction expressions (which we frequently need for calculating pointer offsets). */ const __ptrAdd = function(...args){ let rc = __NullPtr; for( let i = 0; i < args.length; ++i ){ rc += __asPtrType(args[i]); } return rc; }; if(!SBF.debugFlags){ SBF.__makeDebugFlags = function(deriveFrom=null){ /* This is disgustingly overengineered. :/ */ if(deriveFrom && deriveFrom.__flags) deriveFrom = deriveFrom.__flags; const f = function f(flags){ if(0===arguments.length){ return f.__flags; } if(flags<0){ delete f.__flags.getter; delete f.__flags.setter; delete f.__flags.alloc; delete f.__flags.dealloc; }else{ f.__flags.getter = 0!==(0x01 & flags); f.__flags.setter = 0!==(0x02 & flags); f.__flags.alloc = 0!==(0x04 & flags); f.__flags.dealloc = 0!==(0x08 & flags); } return f._flags; }; Object.defineProperty(f,'__flags', { iterable: false, writable: false, value: Object.create(deriveFrom) }); if(!deriveFrom) f(0); return f; }; SBF.debugFlags = SBF.__makeDebugFlags(); }/*static init*/ const isLittleEndian = (function() { const buffer = new ArrayBuffer(2); new DataView(buffer).setInt16(0, 256, true /* littleEndian */); // Int16Array uses the platform's endianness. return new Int16Array(buffer)[0] === 256; })(); /** Some terms used in the internal docs: StructType: a struct-wrapping class generated by this framework. DEF: struct description object. SIG: struct member signature string. */ /** True if SIG s looks like a function signature, else false. */ const isFuncSig = (s)=>'('===s[1]; /** True if SIG s is-a pointer-type signature. */ const isPtrSig = (s)=>'p'===s || 'P'===s || 's'===s; const isAutoPtrSig = (s)=>'P'===s /*EXPERIMENTAL*/; /** Returns p if SIG s is a function SIG, else returns s[0]. */ const sigLetter = (s)=>s ? (isFuncSig(s) ? 'p' : s[0]) : undefined; /** Returns the WASM IR form of the Emscripten-conventional letter at SIG s[0]. Throws for an unknown SIG. */ const sigIR = function(s){ switch(sigLetter(s)){ case 'c': case 'C': return 'i8'; case 'i': return 'i32'; case 'p': case 'P': case 's': return ptrIR; case 'j': return 'i64'; case 'f': return 'float'; case 'd': return 'double'; } toss("Unhandled signature IR:",s); }; /** Returns the WASM sizeof of the Emscripten-conventional letter at SIG s[0]. Throws for an unknown SIG. */ const sigSize = function(s){ switch(sigLetter(s)){ case 'c': case 'C': return 1; case 'i': return 4; case 'p': case 'P': case 's': return ptrSize; case 'j': return 8; case 'f': return 4; case 'd': return 8; } toss("Unhandled signature sizeof:",s); }; const affirmBigIntArray = BigInt64Array ? ()=>true : ()=>toss('BigInt64Array is not available.'); /** Returns the name of a DataView getter method corresponding to the given SIG. */ const sigDVGetter = function(s){ switch(sigLetter(s)) { case 'p': case 'P': case 's': { switch(ptrSize){ case 4: return 'getInt32'; case 8: return affirmBigIntArray() && 'getBigInt64'; } break; } case 'i': return 'getInt32'; case 'c': return 'getInt8'; case 'C': return 'getUint8'; case 'j': return affirmBigIntArray() && 'getBigInt64'; case 'f': return 'getFloat32'; case 'd': return 'getFloat64'; } toss("Unhandled DataView getter for signature:",s); }; /** Returns the name of a DataView setter method corresponding to the given SIG. */ const sigDVSetter = function(s){ switch(sigLetter(s)){ case 'p': case 'P': case 's': { switch(ptrSize){ case 4: return 'setInt32'; case 8: return affirmBigIntArray() && 'setBigInt64'; } break; } case 'i': return 'setInt32'; case 'c': return 'setInt8'; case 'C': return 'setUint8'; case 'j': return affirmBigIntArray() && 'setBigInt64'; case 'f': return 'setFloat32'; case 'd': return 'setFloat64'; } toss("Unhandled DataView setter for signature:",s); }; /** Returns a factory for either Number or BigInt, depending on the given SIG. This constructor is used in property setters to coerce the being-set value to the correct pointer size. */ const sigDVSetWrapper = function(s){ switch(sigLetter(s)) { case 'i': case 'f': case 'c': case 'C': case 'd': return Number; case 'j': return __BigInt; case 'p': case 'P': case 's': switch(ptrSize){ case 4: return Number; case 8: return __BigInt; } break; } toss("Unhandled DataView set wrapper for signature:",s); }; /** Returns the given struct and member name in a form suitable for debugging and error output. */ const sPropName = (s,k)=>s+'::'+k; const __propThrowOnSet = function(structName,propName){ return ()=>toss(sPropName(structName,propName),"is read-only."); }; /** In order to completely hide StructBinder-bound struct pointers from JS code, we store them in a scope-local WeakMap which maps the struct-bound objects to their WASM pointers. The pointers are accessible via boundObject.pointer, which is gated behind a property interceptor, but are not exposed anywhere else in the object. This approach means we cannot proxy arrays, or any type which might be realloced, as that pointer could change out from under us. That's not an issue for nested structs, but it might be for a struct they're embedded in. In the case of nested structs we "could" record their top-most parent object and their offset into that object, instead of storing the pointer itself. We could that by allowing a function instead of a pointer in this map, that function returning the (lazily-calculated) address. Hmm. */ const __instancePointerMap = new WeakMap(); const getInstancePtr = (obj)=>__instancePointerMap.get(obj); /** Property name for the pointer-is-external marker. */ const xPtrPropName = Symbol('(pointer-is-external)'); const __isPtr32 = (ptr)=>('number'===typeof ptr && (ptr===(ptr|0)) && ptr>=0); const __isPtr64 = (ptr)=>( ('bigint'===typeof ptr && ptr >= 0) || __isPtr32(ptr) ); /** isPtr() is an alias for isPtr32() or isPtr64(), depending on ptrSize. */ const __isPtr = (4===ptrSize) ? __isPtr32 : __isPtr64; /** Frees the obj.pointer memory and clears the pointer property. */ const __freeStruct = function(ctor, obj, m){ if(!m) m = getInstancePtr(obj); if(m) { __instancePointerMap.delete(obj); if(Array.isArray(obj.ondispose)){ let x; while((x = obj.ondispose.shift())){ try{ if(x instanceof Function) x.call(obj); else if(x instanceof StructType) x.dispose(); else if(__isPtr(x)) dealloc(x); // else ignore. Strings are permitted to annotate entries // to assist in debugging. }catch(e){ console.warn("ondispose() for",ctor.structName,'@', m,'threw. NOT propagating it.',e); } } }else if(obj.ondispose instanceof Function){ try{obj.ondispose()} catch(e){ /*do not rethrow: destructors must not throw*/ console.warn("ondispose() for",ctor.structName,'@', m,'threw. NOT propagating it.',e); } } delete obj.ondispose; if(ctor.debugFlags.__flags.dealloc){ log("debug.dealloc:",(obj[xPtrPropName]?"EXTERNAL":""), ctor.structName,"instance:", ctor.structInfo.sizeof,"bytes @"+m); } if(!obj[xPtrPropName]){ if( ctor.structInfo.zeroOnFree ){ heap().fill(0, Number(m), Number(m)+ctor.structInfo.sizeof); } dealloc(m); } } }; /** Returns a skeleton for a read-only property accessor wrapping value v. */ const rop = (v)=>{return {configurable: false, writable: false, iterable: false, value: v}}; /** Allocates obj's memory buffer based on the size defined in ctor.structInfo.sizeof. */ const __allocStruct = function(ctor, obj, m){ let fill = !m; if(m) Object.defineProperty(obj, xPtrPropName, rop(m)); else{ m = alloc(ctor.structInfo.sizeof); if(!m) toss("Allocation of",ctor.structName,"structure failed."); } try { if(ctor.debugFlags.__flags.alloc){ log("debug.alloc:",(fill?"":"EXTERNAL"), ctor.structName,"instance:", ctor.structInfo.sizeof,"bytes @"+m); } if(fill){ heap().fill(0, Number(m), Number(m) + ctor.structInfo.sizeof); } __instancePointerMap.set(obj, m); }catch(e){ __freeStruct(ctor, obj, m); throw e; } }; /** Gets installed as the memoryDump() method of all structs. */ const __memoryDump = function(){ const p = this.pointer; return p ? new Uint8Array(heap().slice(Number(p), Number(p) + this.structInfo.sizeof)) : null; }; /** True if sig looks like an emscripten/jaccwabyt type signature, else false. */ const looksLikeASig = function f(sig){ f.rxSig1 ??= /^[ipPsjfdcC]$/; f.rxSig2 ??= /^[vipPsjfdcC]\([ipPsjfdcC]*\)$/; return f.rxSig1.test(sig) || f.rxSig2.test(sig); }; /** Returns a pair of adaptor maps (objects) in a length-3 array specific to the given object. */ const __adaptorsFor = function(who){ let x = this.get(who); if( !x ){ x = [ Object.create(null), Object.create(null), Object.create(null) ]; this.set(who, x); } return x; }.bind(new WeakMap); /** Code de-duplifier for __adaptGet(), __adaptSet(), and __adaptStruct(). */ const __adaptor = function(who, which, key, proxy){ const a = __adaptorsFor(who)[which]; if(3===arguments.length) return a[key]; if( proxy ) return a[key] = proxy; return delete a[key]; }; const noopAdapter = (x)=>x; // StructBinder::adaptGet() const __adaptGet = function(key, ...args){ /*if( looksLikeASig(key) ){ toss("Getter adaptor's name (",key,") collides with a data type signature."); }*/ return __adaptor(this, 0, key, ...args); }; // StructBinder::adaptSet() const __adaptSet = function(key, ...args){ if( looksLikeASig(key) ){ toss("Setter adaptor's name (",key,") collides with a data type signature."); } return __adaptor(this, 1, key, ...args); }; // StructBinder::adaptStruct() const __adaptStruct = function(key, ...args){ return __adaptor(this, 2, key, ...args); }; const __adaptStruct2 = function(who,key){ const si = ('string'===typeof key) ? __adaptor(who, 2, key) : key; if( 'object'!==typeof si ){ toss("Invalid struct mapping object. Arg =",key,JSON.stringify(si)); } return si; }; const __memberKey = (k)=>memberPrefix + k + memberSuffix; const __memberKeyProp = rop(__memberKey); //const __adaptGetProp = rop(__adaptGet); /** Looks up a struct member in structInfo.members. Throws if found if tossIfNotFound is true, else returns undefined if not found. The given name may be either the name of the structInfo.members key (faster) or the key as modified by the memberPrefix and memberSuffix settings. */ const __lookupMember = function(structInfo, memberName, tossIfNotFound=true){ let m = structInfo.members[memberName]; if(!m && (memberPrefix || memberSuffix)){ // Check for a match on members[X].key for(const v of Object.values(structInfo.members)){ if(v.key===memberName){ m = v; break; } } if(!m && tossIfNotFound){ toss(sPropName(structInfo.name || structInfo.structName, memberName), 'is not a mapped struct member.'); } } return m; }; /** Uses __lookupMember(obj.structInfo,memberName) to find a member, throwing if not found. Returns its signature, either in this framework's native format or in Emscripten format. */ const __memberSignature = function f(obj,memberName,emscriptenFormat=false){ if(!f._) f._ = (x)=>x.replace(/[^vipPsjrdcC]/g,"").replace(/[pPscC]/g,'i'); const m = __lookupMember(obj.structInfo, memberName, true); return emscriptenFormat ? f._(m.signature) : m.signature; }; const __ptrPropDescriptor = { configurable: false, enumerable: false, get: function(){return getInstancePtr(this)}, set: ()=>toss("Cannot assign the 'pointer' property of a struct.") // Reminder: leaving `set` undefined makes assignments // to the property _silently_ do nothing. Current unit tests // rely on it throwing, though. }; /** Impl of X.memberKeys() for StructType and struct ctors. */ const __structMemberKeys = rop(function(){ const a = []; for(const k of Object.keys(this.structInfo.members)){ a.push(this.memberKey(k)); } return a; }); const __utf8Decoder = new TextDecoder('utf-8'); const __utf8Encoder = new TextEncoder(); /** Internal helper to use in operations which need to distinguish between SharedArrayBuffer heap memory and non-shared heap. */ const __SAB = ('undefined'===typeof SharedArrayBuffer) ? function(){} : SharedArrayBuffer; const __utf8Decode = function(arrayBuffer, begin, end){ if( 8===ptrSize ){ begin = Number(begin); end = Number(end); } return __utf8Decoder.decode( (arrayBuffer.buffer instanceof __SAB) ? arrayBuffer.slice(begin, end) : arrayBuffer.subarray(begin, end) ); }; /** Uses __lookupMember() to find the given obj.structInfo key. Returns that member if it is a string, else returns false. If the member is not found, throws if tossIfNotFound is true, else returns false. */ const __memberIsString = function(obj,memberName, tossIfNotFound=false){ const m = __lookupMember(obj.structInfo, memberName, tossIfNotFound); return (m && 1===m.signature.length && 's'===m.signature[0]) ? m : false; }; /** Given a member description object, throws if member.signature is not valid for assigning to or interpretation as a C-style string. It optimistically assumes that any signature of (i,p,s) is C-string compatible. */ const __affirmCStringSignature = function(member){ if('s'===member.signature) return; toss("Invalid member type signature for C-string value:", JSON.stringify(member)); }; /** Looks up the given member in obj.structInfo. If it has a signature of 's' then it is assumed to be a C-style UTF-8 string and a decoded copy of the string at its address is returned. If the signature is of any other type, it throws. If an s-type member's address is 0, `null` is returned. */ const __memberToJsString = function f(obj,memberName){ const m = __lookupMember(obj.structInfo, memberName, true); __affirmCStringSignature(m); const addr = obj[m.key]; //log("addr =",addr,memberName,"m =",m); if(!addr) return null; let pos = addr; const mem = heap(); for( ; mem[pos]!==0; ++pos ) { //log("mem[",pos,"]",mem[pos]); }; //log("addr =",addr,"pos =",pos); return (addr===pos) ? "" : __utf8Decode(mem, addr, pos); }; /** Adds value v to obj.ondispose, creating ondispose, or converting it to an array, if needed. */ const __addOnDispose = function(obj, ...v){ if(obj.ondispose){ if(!Array.isArray(obj.ondispose)){ obj.ondispose = [obj.ondispose]; } }else{ obj.ondispose = []; } obj.ondispose.push(...v); }; /** Allocates a new UTF-8-encoded, NUL-terminated copy of the given JS string and returns its address relative to heap(). If allocation returns 0 this function throws. Ownership of the memory is transfered to the caller, who must eventually pass it to the configured dealloc() function. */ const __allocCString = function(str){ const u = __utf8Encoder.encode(str); const mem = alloc(u.length+1); if(!mem) toss("Allocation error while duplicating string:",str); const h = heap(); //let i = 0; //for( ; i < u.length; ++i ) h[mem + i] = u[i]; h.set(u, Number(mem)); h[__ptrAdd(mem, u.length)] = 0; //log("allocCString @",mem," =",u); return mem; }; /** Sets the given struct member of obj to a dynamically-allocated, UTF-8-encoded, NUL-terminated copy of str. It is up to the caller to free any prior memory, if appropriate. The newly-allocated string is added to obj.ondispose so will be freed when the object is disposed. The given name may be either the name of the structInfo.members key (faster) or the key as modified by the memberPrefix and memberSuffix settings. */ const __setMemberCString = function(obj, memberName, str){ const m = __lookupMember(obj.structInfo, memberName, true); __affirmCStringSignature(m); /* Potential TODO: if obj.ondispose contains obj[m.key] then dealloc that value and clear that ondispose entry */ const mem = __allocCString(str); obj[m.key] = mem; __addOnDispose(obj, mem); return obj; }; /** Prototype for all StructFactory instances (the constructors returned from StructBinder). */ const StructType = function ctor(structName, structInfo){ if(arguments[2]!==rop){ toss("Do not call the StructType constructor", "from client-level code."); } Object.defineProperties(this,{ //isA: rop((v)=>v instanceof ctor), structName: rop(structName), structInfo: rop(structInfo) }); }; /** Properties inherited by struct-type-specific StructType instances and (indirectly) concrete struct-type instances. */ StructType.prototype = Object.create(null, { dispose: rop(function(){__freeStruct(this.constructor, this)}), lookupMember: rop(function(memberName, tossIfNotFound=true){ return __lookupMember(this.structInfo, memberName, tossIfNotFound); }), memberToJsString: rop(function(memberName){ return __memberToJsString(this, memberName); }), memberIsString: rop(function(memberName, tossIfNotFound=true){ return __memberIsString(this, memberName, tossIfNotFound); }), memberKey: __memberKeyProp, memberKeys: __structMemberKeys, memberSignature: rop(function(memberName, emscriptenFormat=false){ return __memberSignature(this, memberName, emscriptenFormat); }), memoryDump: rop(__memoryDump), pointer: __ptrPropDescriptor, setMemberCString: rop(function(memberName, str){ return __setMemberCString(this, memberName, str); }) }); // Function-type non-Property inherited members Object.assign(StructType.prototype,{ addOnDispose: function(...v){ __addOnDispose(this,...v); return this; } }); /** "Static" properties for StructType. */ Object.defineProperties(StructType, { allocCString: rop(__allocCString), isA: rop((v)=>v instanceof StructType), hasExternalPointer: rop((v)=>(v instanceof StructType) && !!v[xPtrPropName]), memberKey: __memberKeyProp }); /** If struct description object si has a getter proxy, return it (a function), else return undefined. */ const memberGetterProxy = function(si){ return si.get || (si.adaptGet ? StructBinder.adaptGet(si.adaptGet) : undefined); }; /** If struct description object si has a setter proxy, return it (a function), else return undefined. */ const memberSetterProxy = function(si){ return si.set || (si.adaptSet ? StructBinder.adaptSet(si.adaptSet) : undefined); }; /** To be called by makeMemberWrapper() when si has a 'members' member, i.e. is an embedded struct. This function sets up that struct like any other and also sets up property accessor for ctor.memberKey(name) which returns an instance of that new StructType when the member is accessed. That instance wraps the memory of the member's part of the containing C struct instance. That is, if struct Foo has member bar which is an inner struct then: const f = new Foo; const b = f.bar; assert( b is-a StructType object ); assert( b.pointer === f.b.pointer ); b will be disposed of when f() is. Calling b.dispose() will not do any permanent harm, as the wrapper object will be recreated when accessing f.bar, pointing to the same memory in f. The si.zeroOnFree flag has no effect on embedded structs because they wrap "external" memory, so do not own it, and are thus never freed, as such. */ const makeMemberStructWrapper = function callee(ctor, name, si){ /** Where we store inner-struct member proxies. Keys are a combination of the parent object's pointer address and the property's name. The values are StructType instances. */ const __innerStructs = (callee.innerStructs ??= new Map()); const key = ctor.memberKey(name); if( undefined!==si.signature ){ toss("'signature' cannot be used on an embedded struct (", ctor.structName,".",key,")."); } if( memberSetterProxy(si) ){ toss("'set' and 'adaptSet' are not permitted for nested struct members."); } //console.warn("si =",ctor.structName, name, JSON.stringify(si,' ')); si.structName ??= ctor.structName+'::'+name; si.key = key; si.name = name; si.constructor = this.call(this, si.structName, si); //console.warn("si.constructor =",si.constructor); //console.warn("si =",si,"ctor=",ctor); const getterProxy = memberGetterProxy(si); const prop = Object.assign(Object.create(null),{ configurable: false, enumerable: false, set: __propThrowOnSet(ctor/*not si.constructor*/.structName, key), get: function(){ const dbg = si.constructor.prototype.debugFlags.__flags; const p = this.pointer; const k = p+'.'+key; let s = __innerStructs.get(k); if(dbg.getter){ log("debug.getter: k =",k); } if( !s ){ s = new si.constructor(__ptrAdd(p, si.offset)); __innerStructs.set(k, s); this.addOnDispose(()=>s.dispose()); s.addOnDispose(()=>__innerStructs.delete(k)); //console.warn("Created",k,"proxy"); } if(getterProxy) s = getterProxy.apply(this,[s,key]); if(dbg.getter) log("debug.getter: result =",s); return s; } }); Object.defineProperty(ctor.prototype, key, prop); }/*makeMemberStructWrapper()*/; /** Pass this a StructBinderImpl-generated constructor, a member property name, and the struct member description object. It will define property accessors for proto[memberKey] which read from/write to memory in this.pointer. It modifies si to make certain downstream operations simpler. */ const makeMemberWrapper = function f(ctor, name, si){ si = __adaptStruct2(this, si); if( si.members ){ return makeMemberStructWrapper.call(this, ctor, name, si); } if(!f._){ /* Cache all available getters/setters/set-wrappers for direct reuse in each accessor function. */ f._ = {getters: {}, setters: {}, sw:{}}; const a = ['i','c','C','p','P','s','f','d','v()']; if(bigIntEnabled) a.push('j'); a.forEach(function(v){ f._.getters[v] = sigDVGetter(v) /* DataView[MethodName] values for GETTERS */; f._.setters[v] = sigDVSetter(v) /* DataView[MethodName] values for SETTERS */; f._.sw[v] = sigDVSetWrapper(v) /* BigInt or Number ctor to wrap around values for conversion */; }); f.sigCheck = function(obj, name, key,sig){ if(Object.prototype.hasOwnProperty.call(obj, key)){ toss(obj.structName,'already has a property named',key+'.'); } looksLikeASig(sig) || toss("Malformed signature for", sPropName(obj.structName,name)+":",sig); }; } const key = ctor.memberKey(name); f.sigCheck(ctor.prototype, name, key, si.signature); si.key = key; si.name = name; const sigGlyph = sigLetter(si.signature); const xPropName = sPropName(ctor.prototype.structName,key); const dbg = ctor.prototype.debugFlags.__flags; /* TODO?: set prototype of si to an object which can set/fetch its preferred representation, e.g. conversion to string or mapped function. Advantage: we can avoid doing that via if/else if/else in the get/set methods. */ const getterProxy = memberGetterProxy(si); const prop = Object.create(null); prop.configurable = false; prop.enumerable = false; prop.get = function(){ if(dbg.getter){ log("debug.getter:",f._.getters[sigGlyph],"for", sigIR(sigGlyph), xPropName,'@', this.pointer,'+',si.offset,'sz',si.sizeof); } let rc = ( new DataView(heap().buffer, Number(this.pointer) + si.offset, si.sizeof) )[f._.getters[sigGlyph]](0, isLittleEndian); if(getterProxy) rc = getterProxy.apply(this,[rc,key]); if(dbg.getter) log("debug.getter:",xPropName,"result =",rc); return rc; }; if(si.readOnly){ prop.set = __propThrowOnSet(ctor.prototype.structName,key); }else{ const setterProxy = memberSetterProxy(si); prop.set = function(v){ if(dbg.setter){ log("debug.setter:",f._.setters[sigGlyph],"for", sigIR(sigGlyph), xPropName,'@', this.pointer,'+',si.offset,'sz',si.sizeof, v); } if(!this.pointer){ toss("Cannot set native property on a disposed struct instance."); } if( setterProxy ) v = setterProxy.apply(this,[v]); if( null===v || undefined===v ) v = __NullPtr; else while( isPtrSig(si.signature) && !__isPtr(v) ){ if(isAutoPtrSig(si.signature) && (v instanceof StructType)){ // It's a struct instance: let's store its pointer value! v = v.pointer || __NullPtr; if(dbg.setter) log("debug.setter:",xPropName,"resolved to",v); break; } toss("Invalid value for pointer-type",xPropName+'.'); } ( new DataView(heap().buffer, Number(this.pointer) + si.offset, si.sizeof) )[f._.setters[sigGlyph]](0, f._.sw[sigGlyph](v), isLittleEndian); }; } Object.defineProperty(ctor.prototype, key, prop); }/*makeMemberWrapper()*/; /** The main factory function which will be returned to the caller. The third argument is structly for internal use. This level of indirection is to avoid that clients can pass a third argument to this, as that's only for internal use. internalOpt options: - None right now. This is for potential use in recursion. Usages: StructBinder(string, obj [,optObj]); StructBinder(obj); */ const StructBinderImpl = function StructBinderImpl( structName, si, opt = Object.create(null) ){ /** StructCtor is the eventual return value of this function. We need to populate this early on so that we can do some trickery in feeding it through recursion. */ const StructCtor = function StructCtor(externalMemory){ externalMemory = __asPtrType(externalMemory); //console.warn("externalMemory",externalMemory,arguments[0]); if(!(this instanceof StructCtor)){ toss("The",structName,"constructor may only be called via 'new'."); }else if(arguments.length){ if( !__isPtr(externalMemory) ){ toss("Invalid pointer value",arguments[0],"for",structName,"constructor."); } __allocStruct(StructCtor, this, externalMemory); }else{ __allocStruct(StructCtor, this); } }; const self = this; /** "Convert" struct description x to a struct description, if needed. */ const ads = (x)=>{ //console.warn("looksLikeASig(",x,") =",looksLikeASig(x)); return (('string'===typeof x) && looksLikeASig(x)) ? {signature: x} : __adaptStruct2(self,x); }; if(1===arguments.length){ si = ads(structName); structName = si.structName || si.name; }else if(2===arguments.length){ si = ads(si); si.name ??= structName; }else{ si = ads(si); } structName ??= si.structName; //console.warn("arguments =",JSON.stringify(arguments)); structName ??= opt.structName; if( !structName ) toss("One of 'name' or 'structName' are required."); if( si.adapt ){ Object.keys(si.adapt.struct||{}).forEach((k)=>{ __adaptStruct.call(StructBinderImpl, k, si.adapt.struct[k]); }); Object.keys(si.adapt.set||{}).forEach((k)=>{ __adaptSet.call(StructBinderImpl, k, si.adapt.set[k]); }); Object.keys(si.adapt.get||{}).forEach((k)=>{ __adaptGet.call(StructBinderImpl, k, si.adapt.get[k]); }); } if(!si.members && !si.sizeof){ si.sizeof = sigSize(si.signature); } const debugFlags = rop(SBF.__makeDebugFlags(StructBinder.debugFlags)); Object.defineProperties(StructCtor,{ debugFlags: debugFlags, isA: rop((v)=>v instanceof StructCtor), memberKey: __memberKeyProp, memberKeys: __structMemberKeys, //methodInfoForKey: rop(function(mKey){/*???*/}), structInfo: rop(si), structName: rop(structName) }); StructCtor.prototype = new StructType(structName, si, rop); Object.defineProperties(StructCtor.prototype,{ debugFlags: debugFlags, constructor: rop(StructCtor) /*if we assign StructCtor.prototype and don't do this then StructCtor!==instance.constructor*/ }); let lastMember = false; let offset = 0; //console.warn(structName,"si =",si); si.offset ??= 0; Object.keys(si.members || {}).forEach((k)=>{ // Sanity checks of sizeof/offset info... let m = ads(si.members[k]); if(!m.members && !m.sizeof){ /* ^^^^ fixme: we need to recursively collect all sizeofs before updating that. */ m.sizeof = sigSize(m.signature); if(!m.sizeof){ toss(sPropName(structName,k), "is missing a sizeof property.",m); } } if( undefined===m.offset ){ m.offset = offset; } si.members[k] = m; if(!lastMember || lastMember.offset < m.offset) lastMember = m; makeMemberWrapper.call(self, StructCtor, k, m) offset += m.sizeof; //console.warn("offset",sPropName(structName,k),offset); }); if( !lastMember ) toss("No member property descriptions found."); if( !si.sizeof ) si.sizeof = offset; if(si.sizeof===1){ (si.signature === 'c' || si.signature === 'C') || toss("Unexpected sizeof==1 member", sPropName(structName,k), "with signature",si.signature); }else{ // sizes and offsets of size-1 members may be odd values, but // others may not. if(0!==(si.sizeof%4)){ console.warn("Invalid struct member description",si); toss(structName,"sizeof is not aligned. sizeof="+si.sizeof); } if(0!==(si.offset%4)){ console.warn("Invalid struct member description",si); toss(structName,"offset is not aligned. offset="+si.offset); } } if( si.sizeof < offset ){ console.warn("Suspect struct description:",si,"offset =",offset); toss("Mismatch in the calculated vs. the provided sizeof/offset info.", "Expected sizeof",offset,"but got",si.sizeof,"for",si); } return StructCtor; }/*StructBinderImpl*/; const StructBinder = function StructBinder(structName, structInfo){ return (1==arguments.length) ? StructBinderImpl.call(StructBinder, structName) : StructBinderImpl.call(StructBinder, structName, structInfo); }; StructBinder.StructType = StructType; StructBinder.config = config; StructBinder.allocCString = __allocCString; StructBinder.adaptGet = __adaptGet; StructBinder.adaptSet = __adaptSet; StructBinder.adaptStruct = __adaptStruct; if(!StructBinder.debugFlags){ StructBinder.debugFlags = SBF.__makeDebugFlags(SBF.debugFlags); } return StructBinder; }/*StructBinderFactory*/;