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

Extend the sqlite3.wasm function pointer argument converter to be able to handle the "two-layered context" of sqlite3_create_collation() and friends and make use of FuncPtrAdapter to perform JS-to-WASM function conversion for them.

FossilOrigin-Name: 0a60b7215e433f8c50027c70731b11e58d74c90ec5903e66ae42f9c98e40b044
This commit is contained in:
stephan
2022-12-13 08:25:28 +00:00
parent 7ca4312ff2
commit 30f50a2d34
6 changed files with 135 additions and 87 deletions

View File

@ -462,22 +462,11 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
const __collationContextKey = (argIndex,argv)=>{
return 'argv['+argIndex+']:sqlite3@'+argv[0]+
':'+((/*THIS IS WRONG. We can't sensibly use a converted-to-C-string
address here and don't have access to the JS string (IF ANY)
which the user passed in.*/
''+argv[1]
).toLowerCase());
':'+wasm.cstrToJs(argv[1]).toLowerCase()
};
const __ccv2 = wasm.xWrap(
'sqlite3_create_collation_v2', 'int',
'sqlite3*','string','int','*','*','*'
/* int(*xCompare)(void*,int,const void*,int,const void*) */
/* void(*xDestroy(void*) */
);
if(0){
// Problem: we cannot, due to xWrap() arg-passing limitations,
// currently easily/efficiently get a per-collation distinct
// key for purposes of creating distinct FuncPtrAdapter contexts.
'sqlite3*','string','int','*',
new wasm.xWrap.FuncPtrAdapter({
/* int(*xCompare)(void*,int,const void*,int,const void*) */
name: 'xCompare',
@ -492,7 +481,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
bindScope: 'context',
contextKey: __collationContextKey
})
}
);
/**
Works exactly like C's sqlite3_create_collation_v2() except that:
@ -518,18 +507,9 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
);
}
let rc, pfCompare, pfDestroy;
try{
if(xCompare instanceof Function){
pfCompare = wasm.installFunction(xCompare, 'i(pipip)');
}
if(xDestroy instanceof Function){
pfDestroy = wasm.installFunction(xDestroy, 'v(p)');
}
rc = __ccv2(pDb, zName, eTextRep, pArg,
pfCompare || xCompare, pfDestroy || xDestroy);
try{
rc = __ccv2(pDb, zName, eTextRep, pArg, xCompare, xDestroy);
}catch(e){
if(pfCompare) wasm.uninstallFunction(pfCompare);
if(pfDestroy) wasm.uninstallFunction(pfDestroy);
rc = util.sqlite3_wasm_db_error(pDb, e);
}
return rc;
@ -539,7 +519,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
return (5===arguments.length)
? capi.sqlite3_create_collation_v2(pDb,zName,eTextRep,pArg,xCompare,0)
: __dbArgcMismatch(pDb, 'sqlite3_create_collation', 5);
}
};
}/*sqlite3_create_collation() and friends*/

View File

@ -158,6 +158,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
let rc = capi.sqlite3_open_v2(fn, pPtr, oflags, vfsName || 0);
pDb = wasm.peekPtr(pPtr);
checkSqlite3Rc(pDb, rc);
capi.sqlite3_extended_result_codes(pDb, 1);
if(flagsStr.indexOf('t')>=0){
capi.sqlite3_trace_v2(pDb, capi.SQLITE_TRACE_STMT,
__dbTraceToConsole, 0);

View File

@ -1447,21 +1447,21 @@ self.WhWasmUtilInstaller = function(target){
Requires an options object with these properties:
- name (optional): string describing the function binding. This
is solely for debugging and error-reporting purposes. If not
provided, an empty string is assumed.
is solely for debugging and error-reporting purposes. If not
provided, an empty string is assumed.
- signature: an function signature compatible with
jsFuncToWasm().
- signature: a function signature string compatible with
jsFuncToWasm().
- bindScope (string): one of ('transient', 'context',
'singleton'). Bind scopes are:
- transient: it will convert JS functions to WASM only for the
duration of the xWrap()'d function call, using
- 'transient': it will convert JS functions to WASM only for
the duration of the xWrap()'d function call, using
scopedInstallFunction(). Before that call returns, the
WASM-side binding will be uninstalled.
- singleton: holds one function-pointer binding for this
- 'singleton': holds one function-pointer binding for this
instance. If it's called with a different function pointer,
it uninstalls the previous one after converting the new
value. This is only useful for use with "global" functions
@ -1470,23 +1470,19 @@ self.WhWasmUtilInstaller = function(target){
to be mapped to some sort of state object (e.g. an sqlite3*)
then "context" (see below) is the proper mode.
- context: similar to singleton mode but for a given "context",
where the context is a key provided by the user and possibly
dependent on a small amount of call-time context. This mode
is the default if bindScope is _not_ set but a property named
contextKey (described below) is.
- 'context': similar to singleton mode but for a given
"context", where the context is a key provided by the user
and possibly dependent on a small amount of call-time
context. This mode is the default if bindScope is _not_ set
but a property named contextKey (described below) is.
FIXME: the contextKey definition is only useful for very basic
contexts and breaks down with dynamic ones like
sqlite3_create_collation().
- contextKey (function): only used if bindScope is not set or is
'context'. This function gets passed (argIndex,argv), where
argIndex is the index of this function pointer in its
- contextKey (function): is only used if bindScope is not set or
is 'context'. This function gets passed (argIndex,argv), where
argIndex is the index of _this_ function pointer in its
_wrapping_ function's arguments and argv is the _current_
being-xWrap()-processed args array. All arguments to the left
of argIndex will have been processed by xWrap() by the time
this is called. argv[argIndex] will be the value the user
still-being-xWrap()-processed args array. All arguments to the
left of argIndex will have been processed by xWrap() by the
time this is called. argv[argIndex] will be the value the user
passed in to the xWrap()'d function for the argument this
FuncPtrAdapter is mapped to. Arguments to the right of
argv[argIndex] will not yet have been converted before this is
@ -1500,19 +1496,19 @@ self.WhWasmUtilInstaller = function(target){
might return 'T@'+argv[1], or even just argv[1]. Note,
however, that the (X*) argument will not yet have been
processed by the time this is called and should not be used as
part of that key. Similarly, C-string-type keys should not be
used as part of keys because they are normally transient in
this environment.
part of that key because its pre-conversion data type might be
unpredictable. Similarly, care must be taken with C-string-type
arguments: those to the left in argv will, when this is called,
be WASM pointers, whereas those to the right might (and likely
do) have another data type. When using C-strings in keys, never
use their pointers in the key because most C-strings in this
constellation are transient.
Yes, that ^^^ is a bit awkward, but it's what we have.
The constructor only saves the above state for later, and does
not actually bind any functions. Its convertArg() methor is
not actually bind any functions. Its convertArg() method is
called via xWrap() to perform any bindings.
Caveats:
- singleton is globally singleton. This type does not currently
have enough context to apply, e.g., a different singleton for
each (sqlite3*) db handle.
*/
xArg.FuncPtrAdapter = function ctor(opt) {
if(!(this instanceof xArg.FuncPtrAdapter)){
@ -1530,20 +1526,21 @@ self.WhWasmUtilInstaller = function(target){
if(opt.contextKey) this.contextKey = opt.contextKey /*else inherit one*/;
this.isTransient = 'transient'===this.bindScope;
this.isContext = 'context'===this.bindScope;
if( ('singleton'===this.bindScope) ){
this.singleton = [];
}else{
this.singleton = undefined;
}
if( ('singleton'===this.bindScope) ) this.singleton = [];
else this.singleton = undefined;
//console.warn("FuncPtrAdapter()",opt,this);
};
xArg.FuncPtrAdapter.bindScopes = [
'transient', 'context', 'singleton'
];
xArg.FuncPtrAdapter.prototype = {
/* Dummy impl. Overwritten per-instance as needed. */
contextKey: function(argIndex,argv){
return this;
},
/* Returns this objects mapping for the given context key, in the
form of an an array, creating the mapping if needed. The key
may be anything suitable for use in a Map. */
contextMap: function(key){
const cm = (this.__cmap || (this.__cmap = new Map));
let rc = cm.get(key);
@ -1553,11 +1550,28 @@ self.WhWasmUtilInstaller = function(target){
/**
Gets called via xWrap() to "convert" v to a WASM-bound function
pointer. If v is one of (a pointer, null, undefined) then
(v||0) is returned, otherwise v must be a Function, for which
it creates (if needed) a WASM function binding and returns the
WASM pointer to that binding. It will remember the binding for
at least the next call, to avoid recreating the function
unnecessarily.
(v||0) is returned and any earlier function installed by this
mapping _might_, depending on how it's bound, be
uninstalled. If v is not one of those types, it must be a
Function, for which it creates (if needed) a WASM function
binding and returns the WASM pointer to that binding. If this
instance is not in 'transient' mode, it will remember the
binding for at least the next call, to avoid recreating the
function binding unnecessarily.
If it's passed a pointer(ish) value for v, it does _not_
perform any function binding, so this object's bindMode is
irrelevant for such cases.
argIndex is the argv index of _this_ argument in the
being-xWrap()'d call. argv is the current argument list
undergoing xWrap() argument conversion. argv entries to the
left of argIndex will have already undergone transformation and
those to the right will not have (they will have the values the
client-level code passed in, awaiting conversion). The RHS
indexes must never be relied upon for anything because their
types are indeterminate, whereas the LHS values will be
WASM-compatible values by the time this is called.
*/
convertArg: function(v,argIndex,argv){
//console.warn("FuncPtrAdapter.convertArg()",this.signature,this.transient,v);
@ -1569,6 +1583,7 @@ self.WhWasmUtilInstaller = function(target){
if(v instanceof Function){
const fp = __installFunction(v, this.signature, this.isTransient);
if(pair){
/* Replace existing stashed mapping */
if(pair[1]){
try{target.uninstallFunction(pair[1])}
catch(e){/*ignored*/}
@ -1578,7 +1593,9 @@ self.WhWasmUtilInstaller = function(target){
}
return fp;
}else if(target.isPtr(v) || null===v || undefined===v){
if(pair && pair[1]){
if(pair && pair[1] && pair[1]!==v){
/* uninstall stashed mapping and replace stashed mapping with v. */
//console.warn("FuncPtrAdapter is uninstalling function", this.contextKey(argIndex,argv),v);
try{target.uninstallFunction(pair[1])}
catch(e){/*ignored*/}
pair[0] = pair[1] = (v || 0);
@ -1586,11 +1603,12 @@ self.WhWasmUtilInstaller = function(target){
return v || 0;
}else{
throw new TypeError("Invalid FuncPtrAdapter argument type. "+
"Expecting "+(this.name ? this.name+' ' : '')+
"Expecting a function pointer or a "+
(this.name ? this.name+' ' : '')+
"function matching signature "+
this.signature+".");
}
}
}/*convertArg()*/
}/*FuncPtrAdapter.prototype*/;
const __xArgAdapterCheck =
@ -1778,6 +1796,21 @@ self.WhWasmUtilInstaller = function(target){
if(args.length!==xf.length) __argcMismatch(fname, xf.length);
const scope = target.scopedAllocPush();
try{
/*
Maintenance reminder re. arguments passed to convertArgs():
The public interface of argument adapters is that they take
ONE argument and return a (possibly) converted result for
it. The passing-on of arguments after the first is an
internal impl. detail for the sake of FuncPtrAdapter, and
not to be relied on or documented for other cases. The fact
that this is how FuncPtrAdapter.convertArgs() gets its 2nd+
arguments, and how FuncPtrAdapter.contextKey() gets its
args, is also an implementation detail and subject to
change. i.e. the public interface of 1 argument is stable.
The fact that any arguments may be passed in after that one,
and what those arguments are, is _not_ part of the public
interface and is _not_ stable.
*/
for(const i in args) args[i] = cxw.convertArg(argTypes[i], args[i], i, args);
return cxw.convertResult(resultType, xf.apply(null,args));
}finally{

View File

@ -2125,8 +2125,10 @@ self.sqlite3InitModule = sqlite3InitModule;
})/*custom vtab #2*/
////////////////////////////////////////////////////////////////////////
.t('Custom collation', function(sqlite3){
let collationCounter = 0;
let myCmp = function(pArg,n1,p1,n2,p2){
//int (*)(void*,int,const void*,int,const void*)
++collationCounter;
const rc = wasm.exports.sqlite3_strnicmp(p1,p2,(n1<n2?n1:n2));
return rc ? rc : (n1 - n2);
};
@ -2134,18 +2136,51 @@ self.sqlite3InitModule = sqlite3InitModule;
0, myCmp, 0);
this.db.checkRc(rc);
rc = this.db.selectValue("select 'hi' = 'HI' collate mycollation");
T.assert(1===rc);
T.assert(1===rc).assert(1===collationCounter);
rc = this.db.selectValue("select 'hii' = 'HI' collate mycollation");
T.assert(0===rc);
T.assert(0===rc).assert(2===collationCounter);
rc = this.db.selectValue("select 'hi' = 'HIi' collate mycollation");
T.assert(0===rc);
T.assert(0===rc).assert(3===collationCounter);
rc = capi.sqlite3_create_collation(this.db,"hi",capi.SQLITE_UTF8/*not enough args*/);
T.assert(capi.SQLITE_MISUSE === rc);
rc = capi.sqlite3_create_collation_v2(this.db,"hi",0/*wrong encoding*/,0,0,0);
T.assert(capi.SQLITE_FORMAT === rc)
.mustThrowMatching(()=>this.db.checkRc(rc),
/SQLITE_UTF8 is the only supported encoding./);
})
/*
We need to ensure that replacing that collation function does
the right thing. We don't have a handle to the underlying WASM
pointer from here, so cannot verify (without digging through
internal state) that the old one gets uninstalled, but we can
verify that a new one properly replaces it. (That said,
console.warn() output has shown that the uninstallation does
happen.)
*/
collationCounter = 0;
myCmp = function(pArg,n1,p1,n2,p2){
--collationCounter;
return 0;
};
rc = capi.sqlite3_create_collation_v2(this.db, "MYCOLLATION", capi.SQLITE_UTF8,
0, myCmp, 0);
this.db.checkRc(rc);
rc = this.db.selectValue("select 'hi' = 'HI' collate mycollation");
T.assert(rc>0).assert(-1===collationCounter);
rc = this.db.selectValue("select 'a' = 'b' collate mycollation");
T.assert(rc>0).assert(-2===collationCounter);
rc = capi.sqlite3_create_collation_v2(this.db, "MYCOLLATION", capi.SQLITE_UTF8,
0, null, 0);
this.db.checkRc(rc);
rc = 0;
try {
this.db.selectValue("select 'a' = 'b' collate mycollation");
}catch(e){
/* Why is e.resultCode not automatically an extended result
code? The DB() class enables those automatically. */
rc = sqlite3.capi.sqlite3_extended_errcode(this.db);
}
T.assert(capi.SQLITE_ERROR_MISSING_COLLSEQ === rc);
})/*custom collation*/
////////////////////////////////////////////////////////////////////////
.t('Close db', function(){