1
0
mirror of https://github.com/sqlite/sqlite.git synced 2025-07-29 08:01:23 +03:00

Work on an alternate (slightly simpler) approach to binding JS vtabs. Non-eponymous vtabs are not working, for reasons as yet unknown.

FossilOrigin-Name: 6a0fefb93bcccd950df211cf5c2f49660c7b92115dd01b2b508a4ab9e3ab3d23
This commit is contained in:
stephan
2022-12-07 07:22:34 +00:00
parent 30da58c5d6
commit 1eb1b59b89
6 changed files with 659 additions and 301 deletions

View File

@ -16,7 +16,7 @@
*/
'use strict';
self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
const wasm = sqlite3.wasm, capi = sqlite3.capi, toss = sqlite3.util.toss;
const wasm = sqlite3.wasm, capi = sqlite3.capi, toss = sqlite3.util.toss3;
const vh = Object.create(null), vt = Object.create(null);
sqlite3.VfsHelper = vh;
@ -72,21 +72,29 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
/**
Installs a StructBinder-bound function pointer member of the
given name and function in the given StructType target object.
It creates a WASM proxy for the given function and arranges for
that proxy to be cleaned up when tgt.dispose() is called. Throws
that proxy to be cleaned up when tgt.dispose() is called. Throws
on the slightest hint of error, e.g. tgt is-not-a StructType,
name does not map to a struct-bound member, etc.
Returns a proxy for this function which is bound to tgt and takes
2 args (name,func). That function returns the same thing,
permitting calls to be chained.
As a special case, if the given function is a pointer, it is
assumed to be an existing WASM-bound function pointer and is used
as-is with no extra level of proxying or cleanup. Results are
undefined if it's a pointer and it's _not_ a function pointer.
It is legal to pass a value of 0, indicating a NULL pointer, with
the caveat that 0 _is_ a legal function pointer in WASM but it
will not be accepted as such _here_. (Justification: the function
at address zero must be one which initially came from the WASM
module, not a method we want to bind to a virtual table or VFS.)
This function returns a proxy for itself which is bound to tgt
and takes 2 args (name,func). That function returns the same
thing as this one, permitting calls to be chained.
If called with only 1 arg, it has no side effects but returns a
func with the same signature as described above.
If tgt.ondispose is set before this is called then it _must_
be an array, to which this function will append entries.
ACHTUNG: because we cannot generically know how to transform JS
exceptions into result codes, the installed functions do no
automatic catching of exceptions. It is critical, to avoid
@ -94,20 +102,20 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
this function do not throw. The exception, as it were, to that
rule is...
If applyArgcCheck is true then each method gets wrapped in a
proxy which asserts that it is passed the expected number of
arguments, throwing if the argument count does not match
expectations. That is only intended for dev-time usage for sanity
checking, and will leave the C environment in an undefined
state. For non-dev-time use, it is a given that the C API will
never call one of the generated function wrappers with the wrong
argument count.
If applyArgcCheck is true then each JS function (as opposed to
function pointers) gets wrapped in a proxy which asserts that it
is passed the expected number of arguments, throwing if the
argument count does not match expectations. That is only intended
for dev-time usage for sanity checking, and will leave the C
environment in an undefined state.
*/
vh.installMethod = vt.installMethod = function callee(
tgt, name, func, applyArgcCheck = callee.installMethodArgcCheck
){
if(!(tgt instanceof sqlite3.StructBinder.StructType)){
toss("Usage error: target object is-not-a StructType.");
}else if(!(func instanceof Function) && !wasm.isPtr(func)){
toss("Usage errror: expecting a Function or WASM pointer to one.");
}
if(1===arguments.length){
return (n,f)=>callee(tgt, n, f, applyArgcCheck);
@ -143,26 +151,33 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
}/*static init*/
const sigN = tgt.memberSignature(name);
if(sigN.length<2){
toss("Member",name," is not a function pointer. Signature =",sigN);
toss("Member",name,"does not have a function pointer signature:",sigN);
}
const memKey = tgt.memberKey(name);
const fProxy = applyArgcCheck
const fProxy = (applyArgcCheck && !wasm.isPtr(func))
/** This middle-man proxy is only for use during development, to
confirm that we always pass the proper number of
arguments. We know that the C-level code will always use the
correct argument count. */
? callee.argcProxy(tgt, memKey, func, sigN)
: func;
const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
tgt[memKey] = pFunc;
if(!tgt.ondispose) tgt.ondispose = [];
else if(!Array.isArray(tgt.ondispose)) tgt.ondispose = [tgt.ondispose];
if(!tgt.ondispose.__removeFuncList){
tgt.ondispose.push('ondispose.__removeFuncList handler',
if(wasm.isPtr(fProxy)){
if(fProxy && !wasm.functionEntry(fProxy)){
toss("Pointer",fProxy,"is not a WASM function table entry.");
}
tgt[memKey] = fProxy;
}else{
const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
tgt[memKey] = pFunc;
if(!tgt.ondispose) tgt.ondispose = [];
else if(!Array.isArray(tgt.ondispose)) tgt.ondispose = [tgt.ondispose];
if(!tgt.ondispose || !tgt.ondispose.__removeFuncList){
tgt.addOnDispose('ondispose.__removeFuncList handler',
callee.removeFuncList);
tgt.ondispose.__removeFuncList = [];
tgt.ondispose.__removeFuncList = [];
}
tgt.ondispose.__removeFuncList.push(memKey, pFunc);
}
tgt.ondispose.__removeFuncList.push(memKey, pFunc);
return (n,f)=>callee(tgt, n, f, applyArgcCheck);
}/*installMethod*/;
vh.installMethod.installMethodArgcCheck = false;
@ -269,8 +284,9 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
this.installMethods(o.struct, o.methods, !!o.applyArgcCheck);
if('vfs'===key){
if(!o.struct.$zName && 'string'===typeof o.name){
o.struct.$zName = wasm.allocCString(o.name);
/* Note that we leak that C-string. */
o.struct.addOnDispose(
o.struct.$zName = wasm.allocCString(o.name)
);
}
this.registerVfs(o.struct, !!o.asDefault);
}
@ -292,24 +308,20 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
vt.sqlite3ValuesToJs = capi.sqlite3_create_function_v2.udfConvertArgs;
/**
Factory function for wrapXyz() impls.
Factory function for xAbc() impls.
*/
const __xWrapFactory = function(structType){
return function(ptr,remove=false){
const __xWrapFactory = function(methodName,structType){
return function(ptr,removeMapping=false){
if(0===arguments.length) ptr = new structType;
if(ptr instanceof structType){
//T.assert(!this.has(ptr.pointer));
this.set(ptr.pointer, ptr);
return ptr;
}else if(!wasm.isPtr(ptr)){
sqlite3.SQLite3Error.toss("Invalid argument to xWrapFactory");
sqlite3.SQLite3Error.toss("Invalid argument to",methodName+"()");
}
let rc = this.get(ptr);
if(remove) this.delete(ptr);
/*arguable else if(!rc){
rc = new structType(ptr);
this.set(ptr, rc);
}*/
if(removeMapping) this.delete(ptr);
return rc;
}.bind(new Map);
};
@ -336,7 +348,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
called from sqlite3_module::xDisconnect() implementations or
in error handling of a failed xCreate() or xConnect().
*/
vt.xWrapVtab = __xWrapFactory(capi.sqlite3_vtab);
vt.xVtab = __xWrapFactory('xVtab',capi.sqlite3_vtab);
/**
EXPERIMENTAL. DO NOT USE IN CLIENT CODE.
@ -356,7 +368,16 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
intended to be called from xClose() or in error handling of a
failed xOpen().
*/
vt.xWrapCursor = __xWrapFactory(capi.sqlite3_vtab_cursor);
vt.xCursor = __xWrapFactory('xCursor',capi.sqlite3_vtab_cursor);
/**
Convenience form of creating an sqlite3_index_info wrapper,
intended for use in xBestIndex implementations. Note that the
caller is expected to call dispose() on the returned object
before returning. Though not _strictly_ required, as that object
does not own the pIdxInfo memory, it is nonetheless good form.
*/
vt.xIndexInfo = (pIdxInfo)=>new capi.sqlite3_index_info(pIdxInfo);
/**
Given an error object, this function returns
@ -394,27 +415,38 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
let rc = ...
return rc;
}catch(e){
return sqlite3.VtabHelper.xMethodError(
return sqlite3.VtabHelper.xError(
'xColumn', e, sqlite3.capi.SQLITE_XYZ);
// where SQLITE_XYZ is some call-appropriate result code
// defaulting to SQLITE_ERROR.
// where SQLITE_XYZ is some call-appropriate result code.
}
```
If xMethodError.errorReporter is a function, it is called in
If no 3rd argument is provided, its default depends on
the error type:
- An sqlite3.WasmAllocError always resolves to capi.SQLITE_NOMEM.
- If err is an SQLite3Error then its `resultCode` property
is used.
- If all else fails, capi.SQLITE_ERROR is used.
If xError.errorReporter is a function, it is called in
order to report the error, else the error is not reported.
If that function throws, that exception is ignored.
*/
vt.xMethodError = function f(methodName, err, defaultRc=capi.SQLITE_ERROR){
vt.xError = function f(methodName, err, defaultRc){
if(f.errorReporter instanceof Function){
try{f.errorReporter("sqlite3_module::"+methodName+"(): "+err.message);}
catch(e){/*ignored*/}
}
return (err instanceof sqlite3.WasmAllocError)
? capi.SQLITE_NOMEM
: defaultRc;
let rc;
if(err instanceof sqlite3.WasmAllocError) rc = capi.SQLITE_NOMEM;
else if(arguments.length>2) rc = defaultRc;
else if(err instanceof sqlite3.SQLite3Error) rc = err.resultCode;
return rc || capi.SQLITE_ERROR;
};
vt.xMethodError.errorReporter = 1 ? console.error.bind(console) : false;
vt.xError.errorReporter = 1 ? console.error.bind(console) : false;
/**
"The problem" with this is that it introduces an outer function with
@ -426,15 +458,139 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
/** vt.methodCatcher = function(methodName, method, defaultErrRc=capi.SQLITE_ERROR){
return function(...args){
try { method(...args); }
}catch(e){ return vt.xMethodError(methodName, e, defaultRc) }
}catch(e){ return vt.xError(methodName, e, defaultRc) }
};
*/
/**
A helper for sqlite3_vtab::xRow() implementations. It must be
A helper for sqlite3_vtab::xRowid() implementations. It must be
passed that function's 2nd argument and the value for that
pointer. Returns the same as wasm.setMemValue() and will throw
if the 1st or 2nd arguments are invalid for that function.
*/
vt.setRowId = (ppRowid64, value)=>wasm.setMemValue(ppRowid64, value, 'i64');
vt.xRowid = (ppRowid64, value)=>wasm.setMemValue(ppRowid64, value, 'i64');
/**
Sets up an sqlite3_module() object for later installation into
individual databases using sqlite3_create_module(). Requires an
object with the following properties:
- `methods`: an object containing a mapping of properties with
the C-side names of the sqlite3_module methods, e.g. xCreate,
xBestIndex, etc., to JS implementations for those functions.
Certain special-case handling is performed, as described below.
- `catchExceptions` (default=false): if truthy, the given methods
are not mapped as-is, but are instead wrapped inside wrappers
which translate exceptions into result codes of SQLITE_ERROR or
SQLITE_NOMEM, depending on whether the exception is an
sqlite3.WasmAllocError. In the case of the xConnect and xCreate
methods, the exception handler also sets the output error
string to the exception's error string.
- OPTIONAL `struct`: a sqlite3.capi.sqlite3_module() instance. If
not set, one will be created automatically and (on success)
added to the object.
- OPTIONAL `iVersion`: if set, it must be an integer value and it
gets assigned to the `$iVersion` member of the struct object.
If it's _not_ set, and the passed-in `struct` object's `$iVersion`
is 0 (the default) then this function attempts to define a value
for that property based on the list of methods it has.
If `catchExceptions` is false, it is up to the client to ensure
that no exceptions escape the methods, as doing so would move
them through the C API, leading to undefined
behavior. (VtabHelper.xError() is intended to assist in reporting
such exceptions.)
If `methods.xConnect` is `true` then the value of
`methods.xCreate` is used in its place, and vice versa. This is
to facilitate creation of those methods inline in the passed-in
object without requiring the client to explicitly get a reference
to one of them in order to assign it to the other one. Note that
sqlite treats those two functions specially if they are exactly
the same function (same pointer value). The
`catchExceptions`-installed handlers will account for identical
references to those two functions and will install the same
wrapper function for both.
The given methods are expected to return integer values, as
expected by the C API. If `catchExceptions` is truthy, the return
value of the wrapped function will be used as-is and will be
translated to 0 if the function returns a falsy value (e.g. if it
does not have an explicit return). If `catchExceptions` is _not_
active, the method implementations must explicitly return integer
values.
Throws on error. Returns the sqlite3_module object on success.
*/
vt.setupModule = function(opt){
const mod = opt.struct || new capi.sqlite3_module();
try{
const methods = opt.methods || toss("Missing 'methods' object.");
if(true===methods.xConnect) methods.xConnect = methods.xCreate;
else if(true===methods.xCreate) methods.xCreate = methods.xConnect;
if(opt.catchExceptions){
const fwrap = function(methodName, func){
if(['xConnect','xCreate'].indexOf(methodName) >= 0){
return function(pDb, pAux, argc, argv, ppVtab, pzErr){
try{return func(...arguments) || 0;}
catch(e){
if(!(e instanceof sqlite3.WasmAllocError)){
wasm.setPtrValue(pzErr, wasm.allocCString(e.message));
}
return vt.xError(methodName, e);
}
};
}else{
return function(...args){
try{return func(...args) || 0;}
catch(e){
return vt.xError(methodName, e);
}
};
}
};
const mnames = [
'xCreate', 'xConnect', 'xBestIndex', 'xDisconnect',
'xDestroy', 'xOpen', 'xClose', 'xFilter', 'xNext',
'xEof', 'xColumn', 'xRowid', 'xUpdate',
'xBegin', 'xSync', 'xCommit', 'xRollback',
'xFindFunction', 'xRename', 'xSavepoint', 'xRelease',
'xRollbackTo', 'xShadowName'
];
const remethods = Object.create(null);
for(const k of mnames){
const m = methods[k];
if(!(m instanceof Function)) continue;
else if('xConnect'===k && methods.xCreate===m){
remethods[k] = methods.xCreate;
}else if('xCreate'===k && methods.xConnect===m){
remethods[k] = methods.xConnect;
}else{
remethods[k] = fwrap(k, m);
}
}
this.installMethods(mod, remethods, false);
}else{
this.installMethods(
mod, methods, !!opt.applyArgcCheck/*undocumented option*/
);
}
if(0===mod.$iVersion){
let v;
if('number'===typeof opt.iVersion) v = opt.iVersion;
else if(mod.$xShadowName) v = 3;
else if(mod.$xSavePoint || mod.$xRelease || mod.$xRollbackTo) v = 2;
else v = 1;
mod.$iVersion = v;
}
}catch(e){
if(!opt.struct) mod.dispose();
throw e;
}
if(!opt.struct) opt.struct = mod;
return mod;
}/*setupModule()*/;
}/*sqlite3ApiBootstrap.initializers.push()*/);