sqlite3/ext/wasm/jaccwabyt/jaccwabyt.js

697 lines
25 KiB
JavaScript

/**
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 self-held copy ("self" being
the global window or worker object). 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(' '))};
/**
Implementing function bindings revealed significant
shortcomings in Emscripten's addFunction()/removeFunction()
interfaces:
https://github.com/emscripten-core/emscripten/issues/17323
Until those are resolved, or a suitable replacement can be
implemented, our function-binding API will be more limited
and/or clumsier to use than initially hoped.
*/
if(!(config.heap instanceof WebAssembly.Memory)
&& !(config.heap instanceof Function)){
toss("config.heap must be WebAssembly.Memory instance or a function.");
}
['alloc','dealloc'].forEach(function(k){
(config[k] instanceof Function) ||
toss("Config option '"+k+"' must be a function.");
});
const SBF = StructBinderFactory;
const heap = (config.heap instanceof Function)
? config.heap : (()=>new Uint8Array(config.heap.buffer)),
alloc = config.alloc,
dealloc = config.dealloc,
log = config.log || console.log.bind(console),
memberPrefix = (config.memberPrefix || ""),
memberSuffix = (config.memberSuffix || ""),
bigIntEnabled = (undefined===config.bigIntEnabled
? !!self['BigInt64Array'] : !!config.bigIntEnabled),
BigInt = self['BigInt'],
BigInt64Array = self['BigInt64Array'],
/* Undocumented (on purpose) config options: */
ptrSizeof = config.ptrSizeof || 4,
ptrIR = config.ptrIR || 'i32'
;
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 signature. */
const isPtrSig = (s)=>'p'===s || 'P'===s;
const isAutoPtrSig = (s)=>'P'===s /*EXPERIMENTAL*/;
const sigLetter = (s)=>isFuncSig(s) ? 'p' : s[0];
/** 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);
};
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(ptrSizeof){
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(ptrSizeof){
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 either Number of BigInt, depending on the given
SIG. This constructor is used in property setters to coerce
the being-set value to the correct size.
*/
const sigDVSetWrapper = function(s){
switch(sigLetter(s)) {
case 'i': case 'f': case 'c': case 'C': case 'd': return Number;
case 'j': return affirmBigIntArray() && BigInt;
case 'p': case 'P': case 's':
switch(ptrSizeof){
case 4: return Number;
case 8: return affirmBigIntArray() && 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 an accessor
function, but are not exposed anywhere else in the
object. The main intention of that is to make it impossible
for stale copies to be made.
*/
const __instancePointerMap = new WeakMap();
/** Property name for the pointer-is-external marker. */
const xPtrPropName = '(pointer-is-external)';
/** Frees the obj.pointer memory and clears the pointer
property. */
const __freeStruct = function(ctor, obj, m){
if(!m) m = __instancePointerMap.get(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('number' === typeof 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]) 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, m, 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(p, p+this.structInfo.sizeof))
: null;
};
const __memberKey = (k)=>memberPrefix + k + memberSuffix;
const __memberKeyProp = rop(__memberKey);
/**
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,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 __instancePointerMap.get(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){
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, mem);
h[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
});
const isNumericValue = (v)=>Number.isFinite(v) || (v instanceof (BigInt || Number));
/**
Pass this a StructBinder-generated prototype, 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 descr to make certain downstream
operations much simpler.
*/
const makeMemberWrapper = function f(ctor,name, descr){
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){
//const ir = sigIR(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 */;
});
const rxSig1 = /^[ipPsjfdcC]$/,
rxSig2 = /^[vipPsjfdcC]\([ipPsjfdcC]*\)$/;
f.sigCheck = function(obj, name, key,sig){
if(Object.prototype.hasOwnProperty.call(obj, key)){
toss(obj.structName,'already has a property named',key+'.');
}
rxSig1.test(sig) || rxSig2.test(sig)
|| toss("Malformed signature for",
sPropName(obj.structName,name)+":",sig);
};
}
const key = ctor.memberKey(name);
f.sigCheck(ctor.prototype, name, key, descr.signature);
descr.key = key;
descr.name = name;
const sigGlyph = sigLetter(descr.signature);
const xPropName = sPropName(ctor.prototype.structName,key);
const dbg = ctor.prototype.debugFlags.__flags;
/*
TODO?: set prototype of descr to an object which can set/fetch
its prefered 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 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,'+',descr.offset,'sz',descr.sizeof);
}
let rc = (
new DataView(heap().buffer, this.pointer + descr.offset, descr.sizeof)
)[f._.getters[sigGlyph]](0, isLittleEndian);
if(dbg.getter) log("debug.getter:",xPropName,"result =",rc);
return rc;
};
if(descr.readOnly){
prop.set = __propThrowOnSet(ctor.prototype.structName,key);
}else{
prop.set = function(v){
if(dbg.setter){
log("debug.setter:",f._.setters[sigGlyph],"for", sigIR(sigGlyph),
xPropName,'@', this.pointer,'+',descr.offset,'sz',descr.sizeof, v);
}
if(!this.pointer){
toss("Cannot set struct property on disposed instance.");
}
if(null===v) v = 0;
else while(!isNumericValue(v)){
if(isAutoPtrSig(descr.signature) && (v instanceof StructType)){
// It's a struct instance: let's store its pointer value!
v = v.pointer || 0;
if(dbg.setter) log("debug.setter:",xPropName,"resolved to",v);
break;
}
toss("Invalid value for pointer-type",xPropName+'.');
}
(
new DataView(heap().buffer, this.pointer + descr.offset, descr.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.
*/
const StructBinder = function StructBinder(structName, structInfo){
if(1===arguments.length){
structInfo = structName;
structName = structInfo.name;
}else if(!structInfo.name){
structInfo.name = structName;
}
if(!structName) toss("Struct name is required.");
let lastMember = false;
Object.keys(structInfo.members).forEach((k)=>{
// Sanity checks of sizeof/offset info...
const m = structInfo.members[k];
if(!m.sizeof) toss(structName,"member",k,"is missing sizeof.");
else if(m.sizeof===1){
(m.signature === 'c' || m.signature === 'C') ||
toss("Unexpected sizeof==1 member",
sPropName(structInfo.name,k),
"with signature",m.signature);
}else{
// sizes and offsets of size-1 members may be odd values, but
// others may not.
if(0!==(m.sizeof%4)){
console.warn("Invalid struct member description =",m,"from",structInfo);
toss(structName,"member",k,"sizeof is not aligned. sizeof="+m.sizeof);
}
if(0!==(m.offset%4)){
console.warn("Invalid struct member description =",m,"from",structInfo);
toss(structName,"member",k,"offset is not aligned. offset="+m.offset);
}
}
if(!lastMember || lastMember.offset < m.offset) lastMember = m;
});
if(!lastMember) toss("No member property descriptions found.");
else if(structInfo.sizeof < lastMember.offset+lastMember.sizeof){
toss("Invalid struct config:",structName,
"max member offset ("+lastMember.offset+") ",
"extends past end of struct (sizeof="+structInfo.sizeof+").");
}
const debugFlags = rop(SBF.__makeDebugFlags(StructBinder.debugFlags));
/** Constructor for the StructCtor. */
const StructCtor = function StructCtor(externalMemory){
if(!(this instanceof StructCtor)){
toss("The",structName,"constructor may only be called via 'new'.");
}else if(arguments.length){
if(externalMemory!==(externalMemory|0) || externalMemory<=0){
toss("Invalid pointer value for",structName,"constructor.");
}
__allocStruct(StructCtor, this, externalMemory);
}else{
__allocStruct(StructCtor, this);
}
};
Object.defineProperties(StructCtor,{
debugFlags: debugFlags,
isA: rop((v)=>v instanceof StructCtor),
memberKey: __memberKeyProp,
memberKeys: __structMemberKeys,
methodInfoForKey: rop(function(mKey){
}),
structInfo: rop(structInfo),
structName: rop(structName)
});
StructCtor.prototype = new StructType(structName, structInfo, rop);
Object.defineProperties(StructCtor.prototype,{
debugFlags: debugFlags,
constructor: rop(StructCtor)
/*if we assign StructCtor.prototype and don't do
this then StructCtor!==instance.constructor!*/
});
Object.keys(structInfo.members).forEach(
(name)=>makeMemberWrapper(StructCtor, name, structInfo.members[name])
);
return StructCtor;
};
StructBinder.StructType = StructType;
StructBinder.config = config;
StructBinder.allocCString = __allocCString;
if(!StructBinder.debugFlags){
StructBinder.debugFlags = SBF.__makeDebugFlags(SBF.debugFlags);
}
return StructBinder;
}/*StructBinderFactory*/;