Move the JS pieces of kvvfs into their own file to facilitate pending feature experimentation.

FossilOrigin-Name: 3c40614285449df259a3444e36f888cfb5e782ea58287914f97f496ea61e9e9f
This commit is contained in:
stephan
2025-11-21 10:49:32 +00:00
parent 1073985be7
commit 40ce52a4e5
8 changed files with 344 additions and 393 deletions
+1
View File
@@ -910,6 +910,7 @@ sqlite3-api.jses += $(dir.api)/sqlite3-vfs-helper.c-pp.js
ifeq (0,$(wasm-bare-bones))
sqlite3-api.jses += $(dir.api)/sqlite3-vtab-helper.c-pp.js
endif
sqlite3-api.jses += $(dir.api)/sqlite3-vfs-kvvfs.c-pp.js
sqlite3-api.jses += $(dir.api)/sqlite3-vfs-opfs.c-pp.js
sqlite3-api.jses += $(dir.api)/sqlite3-vfs-opfs-sahpool.c-pp.js
-102
View File
@@ -988,8 +988,6 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
const notThese = Object.assign(Object.create(null),{
// For each struct to NOT register, map its name to true:
WasmTestStruct: true,
/* We unregister the kvvfs VFS from Worker threads below. */
sqlite3_kvvfs_methods: !util.isUIThread(),
/* sqlite3_index_info and friends require int64: */
sqlite3_index_info: !wasm.bigIntEnabled,
sqlite3_index_constraint: !wasm.bigIntEnabled,
@@ -1783,106 +1781,6 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
};
}/* auto-extension */
const pKvvfs = capi.sqlite3_vfs_find("kvvfs");
if( pKvvfs ){/* kvvfs-specific glue */
if(util.isUIThread()){
const kvvfsMethods = new capi.sqlite3_kvvfs_methods(
wasm.exports.sqlite3__wasm_kvvfs_methods()
);
delete capi.sqlite3_kvvfs_methods;
const kvvfsMakeKey = wasm.exports.sqlite3__wasm_kvvfsMakeKeyOnPstack,
pstack = wasm.pstack;
const kvvfsStorage = (zClass)=>
((115/*=='s'*/===wasm.peek(zClass))
? sessionStorage : localStorage);
/**
Implementations for members of the object referred to by
sqlite3__wasm_kvvfs_methods(). We swap out the native
implementations with these, which use localStorage or
sessionStorage for their backing store.
*/
const kvvfsImpls = {
xRead: (zClass, zKey, zBuf, nBuf)=>{
const stack = pstack.pointer,
astack = wasm.scopedAllocPush();
try {
const zXKey = kvvfsMakeKey(zClass,zKey);
if(!zXKey) return -3/*OOM*/;
const jKey = wasm.cstrToJs(zXKey);
const jV = kvvfsStorage(zClass).getItem(jKey);
if(!jV) return -1;
const nV = jV.length /* We are relying 100% on v being
ASCII so that jV.length is equal
to the C-string's byte length. */;
if(nBuf<=0) return nV;
else if(1===nBuf){
wasm.poke(zBuf, 0);
return nV;
}
const zV = wasm.scopedAllocCString(jV);
if(nBuf > nV + 1) nBuf = nV + 1;
wasm.heap8u().copyWithin(
Number(zBuf), Number(zV), wasm.ptr.addn(zV, nBuf,- 1)
);
wasm.poke(wasm.ptr.add(zBuf, nBuf, -1), 0);
return nBuf - 1;
}catch(e){
sqlite3.config.error("kvstorageRead()",e);
return -2;
}finally{
pstack.restore(stack);
wasm.scopedAllocPop(astack);
}
},
xWrite: (zClass, zKey, zData)=>{
const stack = pstack.pointer;
try {
const zXKey = kvvfsMakeKey(zClass,zKey);
if(!zXKey) return 1/*OOM*/;
const jKey = wasm.cstrToJs(zXKey);
kvvfsStorage(zClass).setItem(jKey, wasm.cstrToJs(zData));
return 0;
}catch(e){
sqlite3.config.error("kvstorageWrite()",e);
return capi.SQLITE_IOERR;
}finally{
pstack.restore(stack);
}
},
xDelete: (zClass, zKey)=>{
const stack = pstack.pointer;
try {
const zXKey = kvvfsMakeKey(zClass,zKey);
if(!zXKey) return 1/*OOM*/;
kvvfsStorage(zClass).removeItem(wasm.cstrToJs(zXKey));
return 0;
}catch(e){
sqlite3.config.error("kvstorageDelete()",e);
return capi.SQLITE_IOERR;
}finally{
pstack.restore(stack);
}
}
}/*kvvfsImpls*/;
for(const k of Object.keys(kvvfsImpls)){
kvvfsMethods[kvvfsMethods.memberKey(k)] =
wasm.installFunction(
kvvfsMethods.memberSignature(k),
kvvfsImpls[k]
);
}
}else{
/* Worker thread: unregister kvvfs to avoid it being used
for anything other than local/sessionStorage. It "can"
be used that way but it's not really intended to be. */
capi.sqlite3_vfs_unregister(pKvvfs);
delete capi.sqlite3_kvvfs_methods;
}
}/*pKvvfs*/
/* Warn if client-level code makes use of FuncPtrAdapter. */
wasm.xWrap.FuncPtrAdapter.warnOnUse = true;
+1 -50
View File
@@ -239,7 +239,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
*/
ctor._name2vfs = Object.create(null);
const isWorkerThread = ('function'===typeof importScripts/*===running in worker thread*/)
? (n)=>toss3("The VFS for",n,"is only available in the main window thread.")
? (n)=>toss3("VFS",n,"is only available in the main window thread.")
: false;
ctor._name2vfs[':localStorage:'] = {
vfs: 'kvvfs', filename: isWorkerThread || (()=>'local')
@@ -2295,55 +2295,6 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
Stmt
}/*oo1 object*/;
if(util.isUIThread()){
/**
Functionally equivalent to DB(storageName,'c','kvvfs') except
that it throws if the given storage name is not one of 'local'
or 'session'.
As of version 3.46, the argument may optionally be an options
object in the form:
{
filename: 'session'|'local',
... etc. (all options supported by the DB ctor)
}
noting that the 'vfs' option supported by main DB
constructor is ignored here: the vfs is always 'kvvfs'.
*/
sqlite3.oo1.JsStorageDb = function(storageName='session'){
const opt = dbCtorHelper.normalizeArgs(...arguments);
storageName = opt.filename;
if('session'!==storageName && 'local'!==storageName){
toss3("JsStorageDb db name must be one of 'session' or 'local'.");
}
opt.vfs = 'kvvfs';
dbCtorHelper.call(this, opt);
};
const jdb = sqlite3.oo1.JsStorageDb;
jdb.prototype = Object.create(DB.prototype);
/** Equivalent to sqlite3_js_kvvfs_clear(). */
jdb.clearStorage = capi.sqlite3_js_kvvfs_clear;
/**
Clears this database instance's storage or throws if this
instance has been closed. Returns the number of
database blocks which were cleaned up.
*/
jdb.prototype.clearStorage = function(){
return jdb.clearStorage(affirmDbOpen(this).filename);
};
/** Equivalent to sqlite3_js_kvvfs_size(). */
jdb.storageSize = capi.sqlite3_js_kvvfs_size;
/**
Returns the _approximate_ number of bytes this database takes
up in its storage or throws if this instance has been closed.
*/
jdb.prototype.storageSize = function(){
return jdb.storageSize(affirmDbOpen(this).filename);
};
}/*main-window-only bits*/
});
//#else
/* Built with the omit-oo1 flag. */
-80
View File
@@ -1604,86 +1604,6 @@ globalThis.sqlite3ApiBootstrap = async function sqlite3ApiBootstrap(
return x===v ? undefined : x;
}
if( util.isUIThread() ){
/* Features specific to the main window thread... */
/**
Internal helper for sqlite3_js_kvvfs_clear() and friends.
Its argument should be one of ('local','session',"").
*/
const __kvvfsInfo = function(which){
const rc = Object.create(null);
rc.prefix = 'kvvfs-'+which;
rc.stores = [];
if('session'===which || ""===which) rc.stores.push(globalThis.sessionStorage);
if('local'===which || ""===which) rc.stores.push(globalThis.localStorage);
return rc;
};
/**
Clears all storage used by the kvvfs DB backend, deleting any
DB(s) stored there. Its argument must be either 'session',
'local', or "". In the first two cases, only sessionStorage
resp. localStorage is cleared. If it's an empty string (the
default) then both are cleared. Only storage keys which match
the pattern used by kvvfs are cleared: any other client-side
data are retained.
This function is only available in the main window thread.
Returns the number of entries cleared.
*/
capi.sqlite3_js_kvvfs_clear = function(which=""){
let rc = 0;
const kvinfo = __kvvfsInfo(which);
kvinfo.stores.forEach((s)=>{
const toRm = [] /* keys to remove */;
let i;
for( i = 0; i < s.length; ++i ){
const k = s.key(i);
if(k.startsWith(kvinfo.prefix)) toRm.push(k);
}
toRm.forEach((kk)=>s.removeItem(kk));
rc += toRm.length;
});
return rc;
};
/**
This routine guesses the approximate amount of
window.localStorage and/or window.sessionStorage in use by the
kvvfs database backend. Its argument must be one of
('session', 'local', ""). In the first two cases, only
sessionStorage resp. localStorage is counted. If it's an empty
string (the default) then both are counted. Only storage keys
which match the pattern used by kvvfs are counted. The returned
value is the "length" value of every matching key and value,
noting that JavaScript stores each character in 2 bytes.
Note that the returned size is not authoritative from the
perspective of how much data can fit into localStorage and
sessionStorage, as the precise algorithms for determining
those limits are unspecified and may include per-entry
overhead invisible to clients.
*/
capi.sqlite3_js_kvvfs_size = function(which=""){
let sz = 0;
const kvinfo = __kvvfsInfo(which);
kvinfo.stores.forEach((s)=>{
let i;
for(i = 0; i < s.length; ++i){
const k = s.key(i);
if(k.startsWith(kvinfo.prefix)){
sz += k.length;
sz += s.getItem(k).length;
}
}
});
return sz * 2 /* because JS uses 2-byte char encoding */;
};
}/* main-window-only bits */
/**
Wraps all known variants of the C-side variadic
sqlite3_db_config().
+326 -146
View File
@@ -30,19 +30,32 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
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.");
{
let h = config.heap;
if( h instanceof WebAssembly.Memory ){
h = function(){return new Uint8Array(this.buffer)}.bind(h);
}else if( !(h instanceof Function) ){
//console.warn("The bothersome StructBinderFactory config:",config);
toss("config.heap must be WebAssembly.Memory instance or",
"a function which returns one.");
}
config.heap = h;
}
['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),
const heap = config.heap,
alloc = config.alloc,
dealloc = config.dealloc,
realloc = (config.realloc || function(){
toss("This StructBinderFactory was configured without realloc()");
/* We can't know the original memory's size from here unless
we internally proxy alloc()/dealloc() to track all
pointers (not going to happen), so we can't fall back to
doing alloc()/copy/dealloc(). */
}),
log = config.log || console.debug.bind(console),
memberPrefix = (config.memberPrefix || ""),
memberSuffix = (config.memberSuffix || ""),
@@ -51,10 +64,10 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
bigIntEnabled = config.bigIntEnabled ?? !!BigInt64Array;
//console.warn("config",config);
let ptr = alloc(1);
let ptr;
const ptrIR = config.pointerIR
|| config.ptrIR/*deprecated*/
|| ('bigint'===typeof ptr ? 'i64' : 'i32');
|| config.ptrIR/*deprecated*/
|| ('bigint'===typeof (ptr = alloc(1)) ? 'i64' : 'i32');
/* Undocumented (on purpose) config options: */
const ptrSize = config.ptrSize/*deprecated*/
|| ('i32'===ptrIR ? 4 : 8);
@@ -91,6 +104,10 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
return rc;
};
const __ptrAddSelf = function(...args){
return __ptrAdd(this.pointer,...args);
};
if(!SBF.debugFlags){
SBF.__makeDebugFlags = function(deriveFrom=null){
/* This is disgustingly overengineered. :/ */
@@ -120,12 +137,12 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
SBF.debugFlags = SBF.__makeDebugFlags();
}/*static init*/
const isLittleEndian = (function() {
const isLittleEndian = true || (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;
})();
})() /* WASM is, by definition, Little Endian */;
/**
Some terms used in the internal docs:
@@ -146,8 +163,9 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
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. */
/** Returns the WASM IR form of the letter at SIG s[0]. Throws for
an unknown SIG. */
const sigIR = function(s){
switch(sigLetter(s)){
case 'c': case 'C': return 'i8';
@@ -159,8 +177,9 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
}
toss("Unhandled signature IR:",s);
};
/** Returns the WASM sizeof of the Emscripten-conventional letter
at SIG s[0]. Throws for an unknown SIG. */
/** Returns the WASM sizeof of the letter at SIG s[0]. Throws for an
unknown SIG. */
const sigSize = function(s){
switch(sigLetter(s)){
case 'c': case 'C': return 1;
@@ -175,6 +194,7 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
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){
@@ -195,6 +215,7 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
}
toss("Unhandled DataView getter for signature:",s);
};
/** Returns the name of a DataView setter method corresponding
to the given SIG. */
const sigDVSetter = function(s){
@@ -246,26 +267,44 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
/**
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.
the struct-bound objects to an object with their metadata:
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.
{
.p = the native pointer,
.o = self (for an eventual reverse-mapping),
.xb = extra bytes allocated for p,
.zod = zeroOnDispose,
.ownsPointer = true if this object owns p
}
The .p data are accessible via obj.pointer, which is gated behind
a property interceptor, but are not exposed anywhere else in the
public API.
*/
const __instancePointerMap = new WeakMap();
const getInstanceHandle = function f(obj, create=true){
let ii = f.map.get(obj);
if( !ii && create ){
f.map.set(obj, (ii=f.create(obj)));
}
return ii;
};
getInstanceHandle.map = new WeakMap;
getInstanceHandle.create = (forObj)=>{
return Object.assign(Object.create(null),{
o: forObj,
p: undefined/*native ptr*/,
ownsPointer: false,
zod: false/*zeroOnDispose*/,
xb: 0/*extraBytes*/
});
};
const getInstancePtr = (obj)=>__instancePointerMap.get(obj);
/** Property name for the pointer-is-external marker. */
const xPtrPropName = Symbol('(pointer-is-external)');
/**
Remove the getInstanceHandle() mapping for obj.
*/
const rmInstanceHandle = (obj)=>getInstanceHandle.map.delete(obj)
/* If/when we have a reverse map of ptr-to-objects, we need to
clean that here. */;
const __isPtr32 = (ptr)=>('number'===typeof ptr && (ptr===(ptr|0)) && ptr>=0);
const __isPtr64 = (ptr)=>(
@@ -278,85 +317,127 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
*/
const __isPtr = (4===ptrSize) ? __isPtr32 : __isPtr64;
/** Frees the obj.pointer memory and clears the pointer
property. */
const __isNonNullPtr = (v)=>__isPtr(v) && (v>0);
/** Frees the obj.pointer memory (a.k.a. m), handles obj.ondispose,
and unmaps obj from its native resources. */
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*/
const ii = getInstanceHandle(obj, false);
if( !ii ) return;
rmInstanceHandle(obj);
if( !m && !(m = ii.p) ){
console.warn("Cannot(?) happen: __freeStruct() found no instanceInfo");
return;
}
if(Array.isArray(obj.ondispose)){
let x;
while((x = obj.ondispose.pop())){
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);
}
}
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);
}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:",(ii.ownsPointer?"":"EXTERNAL"),
ctor.structName,"instance:",
ctor.structInfo.sizeof,"bytes @"+m);
}
if(ii.ownsPointer){
if( ii.zod || ctor.structInfo.zeroOnDispose ){
heap().fill(0, Number(m),
Number(m) + ctor.structInfo.sizeof + ii.xb);
}
dealloc(m);
}
};
/** Returns a skeleton for a read-only, non-iterable property
* descriptor. */
const rop0 = ()=>{return {configurable: false, writable: false,
iterable: false}};
/** Returns a skeleton for a read-only property accessor wrapping
value v. */
const rop = (v)=>{return {configurable: false, writable: false,
iterable: false, value: v}};
const rop = (v)=>{return {...rop0(), 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.");
const __allocStruct = function f(ctor, obj, xm){
let opt;
const checkPtr = (ptr)=>{
__isNonNullPtr(ptr) ||
toss("Invalid pointer value",arguments[0],"for",ctor.structName,"constructor.");
};
if( arguments.length>=3 ){
if( xm && ('object'===typeof xm) ){
opt = xm;
xm = opt?.wrap;
}else{
checkPtr(xm);
opt = {wrap: xm};
}
}else{
opt = {}
}
const fill = !xm /* true if we need to zero the memory */;
let nAlloc = 0;
let ownsPointer = false;
if(xm){
/* Externally-allocated memory. */
checkPtr(xm);
ownsPointer = !!opt?.takeOwnership;
}else{
const nX = opt?.extraBytes ?? 0;
if( nX<0 || (nX!==(nX|0)) ){
toss("Invalid extraBytes value:",opt?.extraBytes);
}
nAlloc = ctor.structInfo.sizeof + nX;
xm = alloc(nAlloc)
|| toss("Allocation of",ctor.structName,"structure failed.");
ownsPointer = true;
}
try {
if(ctor.debugFlags.__flags.alloc){
if( opt?.debugFlags ){
/* specifically undocumented */
obj.debugFlags(opt.debugFlags);
}
if(ctor./*prototype.???*/debugFlags.__flags.alloc){
log("debug.alloc:",(fill?"":"EXTERNAL"),
ctor.structName,"instance:",
ctor.structInfo.sizeof,"bytes @"+m);
ctor.structInfo.sizeof,"bytes @"+xm);
}
if(fill){
heap().fill(0, Number(m), Number(m) + ctor.structInfo.sizeof);
heap().fill(0, Number(xm), Number(xm) + nAlloc);
}
const ii = getInstanceHandle(obj);
ii.p = xm;
ii.ownsPointer = ownsPointer;
ii.xb = nAlloc ? (nAlloc-ctor.structInfo.sizeof) : 0;
ii.zod = !!opt?.zeroOnDispose;
if( opt?.ondispose && opt.ondispose!==xm ){
obj.addOnDispose( opt.ondispose );
}
__instancePointerMap.set(obj, m);
}catch(e){
__freeStruct(ctor, obj, m);
__freeStruct(ctor, obj, xm);
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. */
@@ -390,25 +471,33 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
// 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);
};
const __affirmNotASig = function(ctx,key){
looksLikeASig(key) &&
toss(ctx,"(",key,") collides with a data type signature.");
};
// StructBinder::adaptSet()
const __adaptSet = function(key, ...args){
if( looksLikeASig(key) ){
toss("Setter adaptor's name (",key,") collides with a data type signature.");
}
__affirmNotASig('Setter adaptor',key);
return __adaptor(this, 1, key, ...args);
};
// StructBinder::adaptStruct()
const __adaptStruct = function(key, ...args){
__affirmNotASig('Struct adaptor',key);
return __adaptor(this, 2, key, ...args);
};
/**
An internal counterpart of __adaptStruct(). If key is-a string,
uses __adaptor(who) to fetch the struct-adaptor entry for key,
else key is assumed to be a struct description object. If it
resolves to an object, that's returned, else an exception is
thrown.
*/
const __adaptStruct2 = function(who,key){
const si = ('string'===typeof key)
? __adaptor(who, 2, key) : key;
@@ -455,15 +544,6 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
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 = [];
@@ -597,13 +677,13 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
Prototype for all StructFactory instances (the constructors
returned from StructBinder).
*/
const StructType = function ctor(structName, structInfo){
if(arguments[2]!==rop){
const StructType = function StructType(structName, structInfo){
if(arguments[2]!==rop/*internal sentinel value*/){
toss("Do not call the StructType constructor",
"from client-level code.");
}
Object.defineProperties(this,{
//isA: rop((v)=>v instanceof ctor),
//isA: rop((v)=>v instanceof StructType),
structName: rop(structName),
structInfo: rop(structInfo)
});
@@ -629,8 +709,31 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
memberSignature: rop(function(memberName, emscriptenFormat=false){
return __memberSignature(this, memberName, emscriptenFormat);
}),
memoryDump: rop(__memoryDump),
pointer: __ptrPropDescriptor,
memoryDump: rop(function(){
const p = this.pointer;
return p
? new Uint8Array(heap().slice(Number(p), Number(p) + this.structInfo.sizeof))
: null;
}),
extraBytes: {
configurable: false, enumerable: false,
get: function(){return getInstanceHandle(this, false)?.xb ?? 0;}
},
zeroOnDispose: {
configurable: false, enumerable: false,
get: function(){
return getInstanceHandle(this, false)?.zod
?? !!this.structInfo.zeroOnDispose;
}
},
pointer: {
configurable: false, enumerable: false,
get: function(){return getInstanceHandle(this, false)?.p},
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.
},
setMemberCString: rop(function(memberName, str){
return __setMemberCString(this, memberName, str);
})
@@ -649,8 +752,12 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
Object.defineProperties(StructType, {
allocCString: rop(__allocCString),
isA: rop((v)=>v instanceof StructType),
hasExternalPointer: rop((v)=>(v instanceof StructType) && !!v[xPtrPropName]),
hasExternalPointer: rop((v)=>{
const ii = getInstanceHandle(v, false);
return !!(ii?.p && !ii?.ownsPointer);
}),
memberKey: __memberKeyProp
//ptrAdd = rop(__ptrAdd) no b/c one might think that it adds based on this.pointer.
});
/**
@@ -693,7 +800,7 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
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
The si.zeroOnDispose flag has no effect on embedded structs because
they wrap "external" memory, so do not own it, and are thus never
freed, as such.
*/
@@ -725,7 +832,7 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
enumerable: false,
set: __propThrowOnSet(ctor/*not si.constructor*/.structName, key),
get: function(){
const dbg = si.constructor.prototype.debugFlags.__flags;
const dbg = this.debugFlags.__flags;
const p = this.pointer;
const k = p+'.'+key;
let s = __innerStructs.get(k);
@@ -758,16 +865,16 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
return makeMemberStructWrapper.call(this, ctor, name, si);
}
if(!f._){
if(!f.cache){
/* Cache all available getters/setters/set-wrappers for
direct reuse in each accessor function. */
f._ = {getters: {}, setters: {}, sw:{}};
f.cache = {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
f.cache.getters[v] = sigDVGetter(v) /* DataView[MethodName] values for GETTERS */;
f.cache.setters[v] = sigDVSetter(v) /* DataView[MethodName] values for SETTERS */;
f.cache.sw[v] = sigDVSetWrapper(v) /* BigInt or Number ctor to wrap around values
for conversion */;
});
f.sigCheck = function(obj, name, key,sig){
@@ -784,8 +891,8 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
si.key = key;
si.name = name;
const sigGlyph = sigLetter(si.signature);
const xPropName = sPropName(ctor.prototype.structName,key);
const dbg = ctor.prototype.debugFlags.__flags;
const xPropName = sPropName(ctor.structName,key);
const dbg = ctor.debugFlags.__flags;
/*
TODO?: set prototype of si to an object which can set/fetch
its preferred representation, e.g. conversion to string or mapped
@@ -797,14 +904,20 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
prop.configurable = false;
prop.enumerable = false;
prop.get = function(){
/**
This getter proxy reads its value from the appropriate pointer
address in the heap. It knows where and how much to read based on
this.pointer, si.offset, and si.sizeof.
*/
if(dbg.getter){
log("debug.getter:",f._.getters[sigGlyph],"for", sigIR(sigGlyph),
log("debug.getter:",f.cache.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]);
)[f.cache.getters[sigGlyph]](0, isLittleEndian);
if(getterProxy) rc = getterProxy.apply(this,[key,rc]);
if(dbg.getter) log("debug.getter:",xPropName,"result =",rc);
return rc;
};
@@ -813,28 +926,33 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
}else{
const setterProxy = memberSetterProxy(si);
prop.set = function(v){
/**
The converse of prop.get(), this encodes v into the appropriate
spot in the WASM heap.
*/
if(dbg.setter){
log("debug.setter:",f._.setters[sigGlyph],"for", sigIR(sigGlyph),
log("debug.setter:",f.cache.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.");
toss("Cannot set native property on a disposed",
this.structSame,"instance.");
}
if( setterProxy ) v = setterProxy.apply(this,[v]);
if( setterProxy ) v = setterProxy.apply(this,[key,v]);
if( null===v || undefined===v ) v = __NullPtr;
else while( isPtrSig(si.signature) && !__isPtr(v) ){
else if( isPtrSig(si.signature) && !__isPtr(v) ){
if(isAutoPtrSig(si.signature) && (v instanceof StructType)){
// It's a struct instance: let's store its pointer value!
// It's a struct instance: store its pointer value
v = v.pointer || __NullPtr;
if(dbg.setter) log("debug.setter:",xPropName,"resolved to",v);
break;
}else{
toss("Invalid value for pointer-type",xPropName+'.');
}
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);
)[f.cache.setters[sigGlyph]](0, f.cache.sw[sigGlyph](v), isLittleEndian);
};
}
Object.defineProperty(ctor.prototype, key, prop);
@@ -863,25 +981,59 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
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.
Uses:
// heap-allocated:
const x = new StructCtor;
// externally-managed memory:
const y = new StructCtor( aPtrToACompatibleCStruct );
or, more recently:
const z = new StructCtor({
extraBytes: [int=0] extra bytes to allocate after the struct
wrap: [aPtrToACompatibleCStruct=undefined]. If provided, this
instance waps, but does not (by default) own the memory, else
a new instance is allocated from the WASM heap.
ownsPointer: true if this object takes over ownership of
wrap.
zeroOnDispose: [bool=StructCtor.structInfo.zeroOnDispose]
autoCalcSizeOffset: [bool=false] Automatically calculate
sizeof an offset. This is fine for pure-JS structs (which
probably aren't useful beyond testing of Jaccwabyt) but it's
dangerous to use with actual WASM objects because we cannot
be guaranteed to have the same memory layout as an ostensibly
matching C struct. This applies recursively to all children
of the struct description.
// TODO? Per-instance overrides of the struct-level flags?
get: (k,v)=>v,
set: (k,v)=>v,
adaptGet: string,
adaptSet: string
// That wouldn't fit really well right now, apparently.
});
*/
const StructCtor = function StructCtor(externalMemory){
externalMemory = __asPtrType(externalMemory);
//console.warn("externalMemory",externalMemory,arguments[0]);
const StructCtor = function StructCtor(arg){
//console.warn("opt",opt,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);
}
__allocStruct(StructCtor, this, ...arguments);
};
const self = this;
/**
"Convert" struct description x to a struct description, if
needed.
needed. This expands adaptStruct() mappings and transforms
{memberName:signatureString} signature syntax to object form.
*/
const ads = (x)=>{
//console.warn("looksLikeASig(",x,") =",looksLikeASig(x));
@@ -902,6 +1054,7 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
structName ??= opt.structName;
if( !structName ) toss("One of 'name' or 'structName' are required.");
if( si.adapt ){
/* Install adaptGet(), adaptSet(), and adaptStruct() proxies. */
Object.keys(si.adapt.struct||{}).forEach((k)=>{
__adaptStruct.call(StructBinderImpl, k, si.adapt.struct[k]);
});
@@ -924,21 +1077,32 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
memberKeys: __structMemberKeys,
//methodInfoForKey: rop(function(mKey){/*???*/}),
structInfo: rop(si),
structName: rop(structName)
structName: rop(structName),
ptrAdd: rop(__ptrAdd)
});
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*/
this then StructCtor!==instance.constructor*/,
ptrAdd: rop(__ptrAddSelf)
});
let lastMember = false;
let offset = 0;
const autoCalc = !!si.autoCalcSizeOffset;
//console.warn(structName,"si =",si);
si.offset ??= 0;
if( !autoCalc ){
if( !si.sizeof ){
toss(structName,"description is missing its sizeof property.");
}
/*if( undefined===si.offset ){
toss(structName,"description is missing its offset property.");
}*/
si.offset ??= 0;
}else{
si.offset ??= 0;
}
Object.keys(si.members || {}).forEach((k)=>{
// Sanity checks of sizeof/offset info...
let m = ads(si.members[k]);
@@ -951,11 +1115,23 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
}
}
if( undefined===m.offset ){
m.offset = offset;
if( autoCalc ) m.offset = offset;
else{
toss(sPropName(structName,k),"is missing its offset.",
JSON.stringify(m));
}
/* A missing offset on the initial child is okay (it's always
zero), but we don't know for sure that the members are
their natural order, so we don't know, at this point, which
one is "first". */
}
si.members[k] = m;
si.members[k] = m /* in case ads() resolved it to something else */;
if(!lastMember || lastMember.offset < m.offset) lastMember = m;
makeMemberWrapper.call(self, StructCtor, k, m)
const oldAutoCalc = !!m.autoCalc;
if( autoCalc ) m.autoCalcSizeOffset = true;
makeMemberWrapper.call(self, StructCtor, k, m);
if( oldAutoCalc ) m.autoCalcSizeOffset = true;
else delete m.autoCalcSizeOffset;
offset += m.sizeof;
//console.warn("offset",sPropName(structName,k),offset);
});
@@ -979,12 +1155,15 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
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);
/* It is legal for the native struct to be larger, so long as
we're pointing to all the right offsets for the members
exposed here. */
}
delete si.autoCalcSizeOffset;
return StructCtor;
}/*StructBinderImpl*/;
@@ -999,6 +1178,7 @@ globalThis.Jaccwabyt = function StructBinderFactory(config){
StructBinder.adaptGet = __adaptGet;
StructBinder.adaptSet = __adaptSet;
StructBinder.adaptStruct = __adaptStruct;
StructBinder.ptrAdd = __ptrAdd;
if(!StructBinder.debugFlags){
StructBinder.debugFlags = SBF.__makeDebugFlags(SBF.debugFlags);
}
+3 -2
View File
@@ -1027,7 +1027,7 @@ globalThis.sqlite3InitModule = sqlite3InitModule;
let ptr = k1.pointer;
k1.dispose();
T.assert(undefined === k1.pointer).
mustThrowMatching(()=>{k1.$pP=1}, /disposed struct instance/);
mustThrowMatching(()=>{k1.$pP=1}, /disposed/);
}finally{
k1.dispose();
k2.dispose();
@@ -2907,11 +2907,12 @@ globalThis.sqlite3InitModule = sqlite3InitModule;
]);
T.assert(3 === db.selectValue('select count(*) from kvvfs'));
db.close();
db = undefined;
db = new JDb(filename);
db.exec('insert into kvvfs(a) values(4),(5),(6)');
T.assert(6 === db.selectValue('select count(*) from kvvfs'));
}finally{
db.close();
if( db ) db.close();
}
}
}/*kvvfs sanity checks*/)
+12 -12
View File
@@ -1,5 +1,5 @@
C Test\scases\sfor\sthe\sfix\sin\sthe\sprior\scheck-in.
D 2025-11-20T22:46:27.556
C Move\sthe\sJS\spieces\sof\skvvfs\sinto\stheir\sown\sfile\sto\sfacilitate\spending\sfeature\sexperimentation.
D 2025-11-21T10:49:32.462
F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
@@ -578,7 +578,7 @@ F ext/session/sessionwor.test 6fd9a2256442cebde5b2284936ae9e0d54bde692d0f5fd009e
F ext/session/sqlite3session.c b3de195ce668cace9b324599bf6255a70290cbfb5451e826e946f3aee6e64c54
F ext/session/sqlite3session.h 7404723606074fcb2afdc6b72c206072cdb2b7d8ba097ca1559174a80bc26f7a
F ext/session/test_session.c 8766b5973a6323934cb51248f621c3dc87ad2a98f023c3cc280d79e7d78d36fb
F ext/wasm/GNUmakefile 01f5e1bc688911b0c0accb0b152dccb19818cb301a90ba9772e3b329a7f625b0
F ext/wasm/GNUmakefile bff7f432a65bc5152ae5aaf19f480dd16fa7fdd11a451773b7dbcac544c703b3
F ext/wasm/README-dist.txt f01081a850ce38a56706af6b481e3a7878e24e42b314cfcd4b129f0f8427066a
F ext/wasm/README.md 2e87804e12c98f1d194b7a06162a88441d33bb443efcfe00dc6565a780d2f259
F ext/wasm/SQLTester/GNUmakefile e0794f676d55819951bbfae45cc5e8d7818dc460492dc317ce7f0d2eca15caff
@@ -593,9 +593,9 @@ F ext/wasm/api/extern-pre-js.js cc61c09c7a24a07dbecb4c352453c3985170cec12b4e7e7e
F ext/wasm/api/post-js-footer.js a50c1a2c4d008aede7b2aa1f18891a7ee71437c2f415b8aeb3db237ddce2935b
F ext/wasm/api/post-js-header.js d24bd0d065f3489c8b78ddf3ead6321e5d047187a162cd503c41700e03dd1f06
F ext/wasm/api/pre-js.c-pp.js ad2546290e0c8ce5ca2081bff6e85cc25eeb904a3303921f1184290a7ff1b32f
F ext/wasm/api/sqlite3-api-glue.c-pp.js 6ff61b7a7a8fb4367ebb2262d9f95a4b8c43eb6964890af99fd322f6cb1b4f00
F ext/wasm/api/sqlite3-api-oo1.c-pp.js 31dbfd470c91ffd96d77399b749bab6b69e3ba9074188833f97ac13f087cf07b
F ext/wasm/api/sqlite3-api-prologue.js a7cab3e8f78d5a93d7173657677fef18265f48d4e0b1e1755740f734a69cae6d
F ext/wasm/api/sqlite3-api-glue.c-pp.js 9eaed1801be392f6687aa7da8e3a5a41d03de19993d8fe62ee6c52617eab4985
F ext/wasm/api/sqlite3-api-oo1.c-pp.js 8ce38bd4b22aa2b0311c7a8e87e748e06213766fe2141de7574672d103ece255
F ext/wasm/api/sqlite3-api-prologue.js 7004b569624765c5132984bfecee2305bef928a6adf44e0202dacc9cbc5c8e2a
F ext/wasm/api/sqlite3-api-worker1.c-pp.js 1041dd645e8e821c082b628cd8d9acf70c667430f9d45167569633ffc7567938
F ext/wasm/api/sqlite3-license-version-header.js 0c807a421f0187e778dc1078f10d2994b915123c1223fe752b60afdcd1263f89
F ext/wasm/api/sqlite3-opfs-async-proxy.js 9654b565b346dc609b75d15337f20acfa7af7d9d558da1afeb9b6d8eaa404966
@@ -627,7 +627,7 @@ F ext/wasm/fiddle/fiddle.js 84fd75967e0af8b69d3dd849818342227d0f81d13db92e0dcbc6
F ext/wasm/fiddle/index.html a27b8127ef9ecf19612da93b2a6a73bdb3777b5c56b5450bb7200a94bc108ff9
F ext/wasm/index-dist.html db23748044e286773f2768eec287669501703b5d5f72755e8db73607dc54d290
F ext/wasm/index.html 54e27db740695ab2cb296e02d42c4c66b3f11b65797340d19fa6590f5b287da1
F ext/wasm/jaccwabyt/jaccwabyt.js 1e734c624205cdf621f322972dfb0fc8013d573a5882f57492a6830e5ec23e17
F ext/wasm/jaccwabyt/jaccwabyt.js 236464b7c8e2d2540a296ba049b3e812e2697ef8439ac96f0d20716e46ba6809
F ext/wasm/jaccwabyt/jaccwabyt.md 167fc0b624c9bc2c477846e336de9403842d81b1a24fc4d3b24317cb9eba734f
F ext/wasm/mkdist.sh 64d53f469c823ed311f6696f69cec9093f745e467334b34f5ceabdf9de3c5b28 x
F ext/wasm/mkwasmbuilds.c ef42e404236dd98cedb6ecea47b6d2474e3c593633ce6d992d316289dfc442b6
@@ -646,7 +646,7 @@ F ext/wasm/test-opfs-vfs.html 1f2d672f3f3fce810dfd48a8d56914aba22e45c6834e262555
F ext/wasm/test-opfs-vfs.js 1618670e466f424aa289859fe0ec8ded223e42e9e69b5c851f809baaaca1a00c
F ext/wasm/tester1-worker.c-pp.html 0e432ec2c0d99cd470484337066e8d27e7aee4641d97115338f7d962bf7b081a
F ext/wasm/tester1.c-pp.html 52d88fe2c6f21a046030a36410b4839b632f4424028197a45a3d5669ea724ddb
F ext/wasm/tester1.c-pp.js 2c255093205a0dac9dae7475030665c2c9d6dccc857de68ee7daf49aa82e6de8
F ext/wasm/tester1.c-pp.js 015b4133cc3a5fb41d6236a6b39d23d996cc2d61a4877acde31a1f69574d4ce3
F ext/wasm/tests/opfs/concurrency/index.html 657578a6e9ce1e9b8be951549ed93a6a471f4520a99e5b545928668f4285fb5e
F ext/wasm/tests/opfs/concurrency/test.js d08889a5bb6e61937d0b8cbb78c9efbefbf65ad09f510589c779b7cc6a803a88
F ext/wasm/tests/opfs/concurrency/worker.js 0a8c1a3e6ebb38aabbee24f122693f1fb29d599948915c76906681bb7da1d3d2
@@ -2175,8 +2175,8 @@ F tool/version-info.c 33d0390ef484b3b1cb685d59362be891ea162123cea181cb8e6d2cf6dd
F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee87c1b31a7
F tool/warnings.sh d924598cf2f55a4ecbc2aeb055c10bd5f48114793e7ba25f9585435da29e7e98
F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
P 8896185ae0b0af8918aa8ce449f18759e6ae60358afbac1019397bae307b15d5
R 254d017f2dd051b92cbdd62f2ef390cd
U drh
Z 1247e56565ac3eb588c6a5672754d68c
P 4d41bee75eda51251121c8e3903f47941116e5182238a03f41a593c47efb6fcf
R 89e12ac8833b73cf56a70154881e6f51
U stephan
Z 653a6aeee27ed8b2d1b86bc8d2dfe977
# Remove this line to create a well-formed Fossil manifest.
+1 -1
View File
@@ -1 +1 @@
4d41bee75eda51251121c8e3903f47941116e5182238a03f41a593c47efb6fcf
3c40614285449df259a3444e36f888cfb5e782ea58287914f97f496ea61e9e9f