Cherry-pick [c4dab53b8ea3401abd] for sqlite3.wasm.xWrap() optimizations.

FossilOrigin-Name: 9b97412d3aa791870016ab3c6f565b6a6afa1764f98e969833aec093b9b29919
This commit is contained in:
stephan 2022-12-23 18:19:28 +00:00
parent 3547e4997f
commit a9e1d96cd8
4 changed files with 184 additions and 104 deletions

View File

@ -359,7 +359,8 @@ self.WhWasmUtilInstaller = function(target){
/**
Given a function pointer, returns the WASM function table entry
if found, else returns a falsy value.
if found, else returns a falsy value: undefined if fptr is out of
range or null if it's in range but the table entry is empty.
*/
target.functionEntry = function(fptr){
const ft = target.functionTable();
@ -552,13 +553,12 @@ self.WhWasmUtilInstaller = function(target){
cache.scopedAlloc[cache.scopedAlloc.length-1].push(ptr);
}
}catch(e){
if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen);
throw e;
if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen);
throw e;
}
return ptr;
return ptr;
};
/**
Expects a JS function and signature, exactly as for
this.jsFuncToWasm(). It uses that function to create a
@ -612,8 +612,13 @@ self.WhWasmUtilInstaller = function(target){
installFunction() has been called and results are undefined if
ptr was not returned by that function. The returned function
may be passed back to installFunction() to reinstall it.
To simplify certain use cases, if passed a falsy non-0 value
(noting that 0 is a valid function table index), this function
has no side effects and returns undefined.
*/
target.uninstallFunction = function(ptr){
if(!ptr && 0!==ptr) return undefined;
const fi = cache.freeFuncIndexes;
const ft = target.functionTable();
fi.push(ptr);
@ -730,7 +735,7 @@ self.WhWasmUtilInstaller = function(target){
? cache : heapWrappers();
for(const p of (Array.isArray(ptr) ? ptr : [ptr])){
switch (type) {
case 'i1':
case 'i1':
case 'i8': c.HEAP8[p>>0] = value; continue;
case 'i16': c.HEAP16[p>>1] = value; continue;
case 'i32': c.HEAP32[p>>2] = value; continue;
@ -803,7 +808,6 @@ self.WhWasmUtilInstaller = function(target){
/** f64 variant of poke8(). */
target.poke64f = (ptr, value)=>target.poke(ptr, value, 'f64');
/** Deprecated alias for getMemValue() */
target.getMemValue = target.peek;
/** Deprecated alias for peekPtr() */
@ -1351,7 +1355,7 @@ self.WhWasmUtilInstaller = function(target){
const __argcMismatch =
(f,n)=>toss(f+"() requires",n,"argument(s).");
/**
Looks up a WASM-exported function named fname from
target.exports. If found, it is called, passed all remaining
@ -1390,18 +1394,25 @@ self.WhWasmUtilInstaller = function(target){
if(target.bigIntEnabled){
xArg.set('i64', (i)=>BigInt(i));
}
xArg.set('i32', (i)=>(i | 0));
xArg.set('i16', (i)=>((i | 0) & 0xFFFF));
xArg.set('i8', (i)=>((i | 0) & 0xFF));
xArg.set('f32', (i)=>Number(i).valueOf());
xArg.set('float', xArg.get('f32'));
xArg.set('f64', xArg.get('f32'));
xArg.set('double', xArg.get('f64'));
xArg.set('int', xArg.get('i32'));
xResult.set('*', xArg.get(ptrIR));
xResult.set('pointer', xArg.get(ptrIR));
xArg.set('**', xArg.get(ptrIR));
xResult.set('number', (v)=>Number(v));
const __xArgPtr = 'i32' === ptrIR
? ((i)=>(i | 0)) : ((i)=>(BigInt(i) | BigInt(0)));
xArg.set('i32', __xArgPtr )
.set('i16', (i)=>((i | 0) & 0xFFFF))
.set('i8', (i)=>((i | 0) & 0xFF))
.set('f32', (i)=>Number(i).valueOf())
.set('float', xArg.get('f32'))
.set('f64', xArg.get('f32'))
.set('double', xArg.get('f64'))
.set('int', xArg.get('i32'))
.set('null', (i)=>i)
.set(null, xArg.get('null'))
.set('**', __xArgPtr);
xResult.set('*', __xArgPtr)
.set('pointer', __xArgPtr)
.set('number', (v)=>Number(v))
.set('void', (v)=>undefined)
.set('null', (v)=>v)
.set(null, xResult.get('null'));
{ /* Copy certain xArg[...] handlers to xResult[...] and
add pointer-style variants of them. */
@ -1430,15 +1441,17 @@ self.WhWasmUtilInstaller = function(target){
TODO? Permit an Int8Array/Uint8Array and convert it to a string?
Would that be too much magic concentrated in one place, ready to
backfire?
backfire? We handle that at the client level in sqlite3 with a
custom argument converter.
*/
xArg.set('string', function(v){
const __xArgString = function(v){
if('string'===typeof v) return target.scopedAllocCString(v);
return v ? xArg.get(ptrIR)(v) : null;
});
xArg.set('utf8', xArg.get('string'));
xArg.set('pointer', xArg.get('string'));
xArg.set('*', xArg.get('string'));
return v ? __xArgPtr(v) : null;
};
xArg.set('string', __xArgString);
xArg.set('utf8', __xArgString);
xArg.set('pointer', __xArgString);
xArg.set('*', __xArgString);
xResult.set('string', (i)=>target.cstrToJs(i));
xResult.set('utf8', xResult.get('string'));
@ -1452,25 +1465,6 @@ self.WhWasmUtilInstaller = function(target){
try{ return i ? JSON.parse(target.cstrToJs(i)) : null }
finally{ target.dealloc(i) }
});
xResult.set('void', (v)=>undefined);
xResult.set('null', (v)=>v);
if(0){
/***
This idea can't currently work because we don't know the
signature for the func and don't have a way for the user to
convey it. To do this we likely need to be able to match
arg/result handlers by a regex, but that would incur an O(N)
cost as we check the regex one at a time. Another use case for
such a thing would be pseudotypes like "int:-1" to say that
the value will always be treated like -1 (which has a useful
case in the sqlite3 bindings).
*/
xArg.set('func-ptr', function(v){
if(!(v instanceof Function)) return xArg.get(ptrIR);
const f = target.jsFuncToWasm(v, WHAT_SIGNATURE);
});
}
/**
Internal-use-only base class for FuncPtrAdapter and potentially
@ -1537,8 +1531,8 @@ self.WhWasmUtilInstaller = function(target){
value. This is only useful for use with "global" functions
which do not rely on any state other than this function
pointer. If the being-converted function pointer is intended
to be mapped to some sort of state object (e.g. an sqlite3*)
then "context" (see below) is the proper mode.
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
@ -1688,35 +1682,41 @@ self.WhWasmUtilInstaller = function(target){
(t)=>xResult.get(t) || toss("Result adapter not found:",t);
cache.xWrap.convertArg = (t,...args)=>__xArgAdapterCheck(t)(...args);
cache.xWrap.convertArgNoCheck = (t,...args)=>xArg.get(t)(...args);
cache.xWrap.convertResult =
(t,v)=>(null===t ? v : (t ? __xResultAdapterCheck(t)(v) : undefined));
cache.xWrap.convertResultNoCheck =
(t,v)=>(null===t ? v : (t ? xResult.get(t)(v) : undefined));
/**
Creates a wrapper for the WASM-exported function fname. Uses
xGet() to fetch the exported function (which throws on
error) and returns either that function or a wrapper for that
Creates a wrapper for another function which converts the arguments
of the wrapper to argument types accepted by the wrapped function,
then converts the wrapped function's result to another form
for the wrapper.
The first argument must be one of:
- A JavaScript function.
- The name of a WASM-exported function. In the latter case xGet()
is used to fetch the exported function, which throws if it's not
found.
- A pointer into the indirect function table. e.g. a pointer
returned from target.installFunction().
It returns either the passed-in function or a wrapper for that
function which converts the JS-side argument types into WASM-side
types and converts the result type. If the function takes no
arguments and resultType is `null` then the function is returned
as-is, else a wrapper is created for it to adapt its arguments
and result value, as described below.
types and converts the result type.
(If you're familiar with Emscripten's ccall() and cwrap(), this
function is essentially cwrap() on steroids.)
This function's arguments are:
- fname: the exported function's name. xGet() is used to fetch
this, so will throw if no exported function is found with that
name.
- resultType: the name of the result type. A literal `null` means
to return the original function's value as-is (mnemonic: there
is "null" conversion going on). Literal `undefined` or the
string `"void"` mean to ignore the function's result and return
`undefined`. Aside from those two special cases, it may be one
of the values described below or any mapping installed by the
client using xWrap.resultAdapter().
The second argument, `resultType`, describes the conversion for
the wrapped functions result. A literal `null` or the string
`'null'` both mean to return the original function's value as-is
(mnemonic: there is "null" conversion going on). Literal
`undefined` or the string `"void"` both mean to ignore the
function's result and return `undefined`. Aside from those two
special cases, the `resultType` value may be one of the values
described below or any mapping installed by the client using
xWrap.resultAdapter().
If passed 3 arguments and the final one is an array, that array
must contain a list of type names (see below) for adapting the
@ -1730,6 +1730,12 @@ self.WhWasmUtilInstaller = function(target){
xWrap('funcname', 'i32', ['string', 'f64']);
```
This function enforces that the given list of arguments has the
same arity as the being-wrapped function (as defined by its
`length` property) and it will throw if that is not the case.
Similarly, the created wrapper will throw if passed a differing
argument count.
Type names are symbolic names which map the arguments to an
adapter function to convert, if needed, the value before passing
it on to WASM or to convert a return result from WASM. The list
@ -1766,6 +1772,10 @@ self.WhWasmUtilInstaller = function(target){
Non-numeric conversions include:
- `null` literal or `"null"` string (args and results): perform
no translation and pass the arg on as-is. This is primarily
useful for results but may have a use or two for arguments.
- `string` or `utf8` (args): has two different semantics in order
to accommodate various uses of certain C APIs
(e.g. output-style strings)...
@ -1780,9 +1790,9 @@ self.WhWasmUtilInstaller = function(target){
client has already allocated and it's passed on as
a WASM pointer.
- `string` or `utf8` (results): treats the result value as a
const C-string, encoded as UTF-8, copies it to a JS string,
and returns that JS string.
- `string` or `utf8` (results): treats the result value as a
const C-string, encoded as UTF-8, copies it to a JS string,
and returns that JS string.
- `string:dealloc` or `utf8:dealloc) (results): treats the result value
as a non-const UTF-8 C-string, ownership of which has just been
@ -1819,6 +1829,11 @@ self.WhWasmUtilInstaller = function(target){
type conversions are valid for both arguments _and_ result types
as they often have different memory ownership requirements.
Design note: the ability to pass in a JS function as the first
argument is of relatively limited use, primarily for testing
argument and result converters. JS functions, by and large, will
not want to deal with C-type arguments.
TODOs:
- Figure out how/whether we can (semi-)transparently handle
@ -1839,14 +1854,21 @@ self.WhWasmUtilInstaller = function(target){
abstracting it into this API (and taking on the associated
costs) may well not make good sense.
*/
target.xWrap = function(fname, resultType, ...argTypes){
target.xWrap = function(fArg, resultType, ...argTypes){
if(3===arguments.length && Array.isArray(arguments[2])){
argTypes = arguments[2];
}
const xf = target.xGet(fname);
if(argTypes.length!==xf.length) __argcMismatch(fname, xf.length);
if(target.isPtr(fArg)){
fArg = target.functionEntry(fArg)
|| toss("Function pointer not found in WASM function table.");
}
const fIsFunc = (fArg instanceof Function);
const xf = fIsFunc ? fArg : target.xGet(fArg);
if(fIsFunc) fArg = xf.name || 'unnamed function';
if(argTypes.length!==xf.length) __argcMismatch(fArg, xf.length);
if((null===resultType) && 0===xf.length){
/* Func taking no args with an as-is return. We don't need a wrapper. */
/* Func taking no args with an as-is return. We don't need a wrapper.
We forego the argc check here, though. */
return xf;
}
/*Verify the arg type conversions are valid...*/;
@ -1859,11 +1881,11 @@ self.WhWasmUtilInstaller = function(target){
if(0===xf.length){
// No args to convert, so we can create a simpler wrapper...
return (...args)=>(args.length
? __argcMismatch(fname, xf.length)
? __argcMismatch(fArg, xf.length)
: cxw.convertResult(resultType, xf.call(null)));
}
return function(...args){
if(args.length!==xf.length) __argcMismatch(fname, xf.length);
if(args.length!==xf.length) __argcMismatch(fArg, xf.length);
const scope = target.scopedAllocPush();
try{
/*
@ -1881,8 +1903,8 @@ self.WhWasmUtilInstaller = function(target){
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));
for(const i in args) args[i] = cxw.convertArgNoCheck(argTypes[i], args[i], i, args);
return cxw.convertResultNoCheck(resultType, xf.apply(null,args));
}finally{
target.scopedAllocPop(scope);
}
@ -1974,14 +1996,13 @@ self.WhWasmUtilInstaller = function(target){
/**
Functions like xCall() but performs argument and result type
conversions as for xWrap(). The first argument is the name of the
exported function to call. The 2nd its the name of its result
type, as documented for xWrap(). The 3rd is an array of argument
type name, as documented for xWrap() (use a falsy value or an
empty array for nullary functions). The 4th+ arguments are
arguments for the call, with the special case that if the 4th
argument is an array, it is used as the arguments for the
call. Returns the converted result of the call.
conversions as for xWrap(). The first, second, and third
arguments are as documented for xWrap(), except that the 3rd
argument may be either a falsy value or empty array to represent
nullary functions. The 4th+ arguments are arguments for the call,
with the special case that if the 4th argument is an array, it is
used as the arguments for the call. Returns the converted result
of the call.
This is just a thin wrapper around xWrap(). If the given function
is to be called more than once, it's more efficient to use
@ -1990,9 +2011,9 @@ self.WhWasmUtilInstaller = function(target){
arguably more efficient because it will hypothetically free the
wrapper function quickly.
*/
target.xCallWrapped = function(fname, resultType, argTypes, ...args){
target.xCallWrapped = function(fArg, resultType, argTypes, ...args){
if(Array.isArray(arguments[3])) args = arguments[3];
return target.xWrap(fname, resultType, argTypes||[]).apply(null, args||[]);
return target.xWrap(fArg, resultType, argTypes||[]).apply(null, args||[]);
};
/**

View File

@ -539,7 +539,7 @@ self.sqlite3InitModule = sqlite3InitModule;
}
w.dealloc(m);
}
// isPtr32()
{
const ip = w.isPtr32;
@ -743,6 +743,65 @@ self.sqlite3InitModule = sqlite3InitModule;
.assert('HI' === cj(new Uint8Array([72, 73])));
});
// jsFuncToWasm()
{
const fsum3 = (x,y,z)=>x+y+z;
fw = w.jsFuncToWasm('i(iii)', fsum3);
T.assert(fw instanceof Function)
.assert( fsum3 !== fw )
.assert( 3 === fw.length )
.assert( 6 === fw(1,2,3) );
T.mustThrowMatching( ()=>w.jsFuncToWasm('x()', function(){}),
'Invalid signature letter: x');
}
// xWrap(Function,...)
{
let fp;
try {
const fmy = function fmy(i,s,d){
if(fmy.debug) log("fmy(",...arguments,")");
T.assert( 3 === i )
.assert( w.isPtr(s) )
.assert( w.cstrToJs(s) === 'a string' )
.assert( T.eqApprox(1.2, d) );
return w.allocCString("hi");
};
fmy.debug = false;
const xwArgs = ['string:dealloc', ['i32', 'string', 'f64']];
fw = w.xWrap(fmy, ...xwArgs);
const fmyArgs = [3, 'a string', 1.2];
let rc = fw(...fmyArgs);
T.assert( 'hi' === rc );
if(0){
/* Retain this as a "reminder to self"...
This extra level of indirection does not work: the
string argument is ending up as a null in fmy() but
the numeric arguments are making their ways through
What's happening is: installFunction() is creating a
WASM-compatible function instance. When we pass a JS string
into there it's getting coerced into `null` before being passed
on to the lower-level wrapper.
*/
fmy.debug = true;
fp = wasm.installFunction('i(isd)', fw);
fw = w.functionEntry(fp);
rc = fw(...fmyArgs);
log("rc =",rc);
T.assert( 'hi' === rc );
// Similarly, this does not work:
//let fpw = w.xWrap(fp, null, [null,null,null]);
//rc = fpw(...fmyArgs);
//log("rc =",rc);
//T.assert( 'hi' === rc );
}
}finally{
wasm.uninstallFunction(fp);
}
}
if(haveWasmCTests()){
if(!sqlite3.config.useStdAlloc){
fw = w.xWrap('sqlite3_wasm_test_str_hello', 'utf8:dealloc',['i32']);
@ -768,7 +827,7 @@ self.sqlite3InitModule = sqlite3InitModule;
});
}
}
}
}/*xWrap()*/
}/*WhWasmUtil*/)
////////////////////////////////////////////////////////////////////
@ -997,7 +1056,6 @@ self.sqlite3InitModule = sqlite3InitModule;
P.restore(stack);
}
}/*pstack tests*/)
////////////////////////////////////////////////////////////////////
;/*end of C/WASM utils checks*/

View File

@ -1,5 +1,5 @@
C Fix\slots\sof\sharmless,\snuisance\scompiler\swarnings,\smostly\sunused\sparameter\nwarnings\sin\sextensions.
D 2022-12-23T14:49:24.143
C Cherry-pick\s[c4dab53b8ea3401abd]\sfor\ssqlite3.wasm.xWrap()\soptimizations.
D 2022-12-23T18:19:28.093
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@ -521,7 +521,7 @@ F ext/wasm/c-pp.c 92285f7bce67ed7b7020b40fde8ed0982c442b63dc33df9dfd4b658d4a6c07
F ext/wasm/common/SqliteTestUtil.js d8bf97ecb0705a2299765c8fc9e11b1a5ac7f10988bbf375a6558b7ca287067b
F ext/wasm/common/emscripten.css 11bd104b6c0d597c67d40cc8ecc0a60dae2b965151e3b6a37fa5708bac3acd15
F ext/wasm/common/testing.css 0ff15602a3ab2bad8aef2c3bd120c7ee3fd1c2054ad2ace7e214187ae68d926f
F ext/wasm/common/whwasmutil.js ba1a8db1f32124e43e24b3d890102b6552b2c0b5a202185041a55887692df328
F ext/wasm/common/whwasmutil.js ff43d28d04e60068ecb3e9a2550581f36be5867ad5ffc00d8479580a12cf9b0f
F ext/wasm/demo-123-worker.html a0b58d9caef098a626a1a1db567076fca4245e8d60ba94557ede8684350a81ed
F ext/wasm/demo-123.html 8c70a412ce386bd3796534257935eb1e3ea5c581e5d5aea0490b8232e570a508
F ext/wasm/demo-123.js ebae30756585bca655b4ab2553ec9236a87c23ad24fc8652115dcedb06d28df6
@ -555,7 +555,7 @@ F ext/wasm/test-opfs-vfs.html 1f2d672f3f3fce810dfd48a8d56914aba22e45c6834e262555
F ext/wasm/test-opfs-vfs.js f09266873e1a34d9bdb6d3981ec8c9e382f31f215c9fd2f9016d2394b8ae9b7b
F ext/wasm/tester1-worker.html d43f3c131d88f10d00aff3e328fed13c858d674ea2ff1ff90225506137f85aa9
F ext/wasm/tester1.c-pp.html d34bef3d48e5cbc1c7c06882ad240fec49bf88f5f65696cc2c72c416933aa406
F ext/wasm/tester1.c-pp.js c45c46cdae1949d426ee12a736087ab180beacc2a20cd829f9052b957adf9ac9
F ext/wasm/tester1.c-pp.js 4202e7ec525445386f29612ddf1365348edd1f6002b8b21721c954b9569b756a
F ext/wasm/tests/opfs/concurrency/index.html 86d8ac435074d1e7007b91105f4897f368c165e8cecb6a9aa3d81f5cf5dcbe70
F ext/wasm/tests/opfs/concurrency/test.js a98016113eaf71e81ddbf71655aa29b0fed9a8b79a3cdd3620d1658eb1cc9a5d
F ext/wasm/tests/opfs/concurrency/worker.js 0a8c1a3e6ebb38aabbee24f122693f1fb29d599948915c76906681bb7da1d3d2
@ -2067,8 +2067,9 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
P 7d3772f0bd0e2602fe919573b49001da4e2b9f3874cb0183dea675204afa7abd
R 16338e89b5ec3a31b67202fe13704742
U drh
Z 80509b64f86f8701ef91d19f5ff36290
P c14bbe1606c1450b709970f922b94a641dfc8f9bd09126501d7dc4db99ea4772
Q +c4dab53b8ea3401abd57671b8f3cb39fa4431b864d4c4e14ae24592f8d4cba0a
R 584560ad77984d30d6316ac32176cf60
U stephan
Z ff71cfe8de9cd6a3b2f8b5e767f940f3
# Remove this line to create a well-formed Fossil manifest.

View File

@ -1 +1 @@
c14bbe1606c1450b709970f922b94a641dfc8f9bd09126501d7dc4db99ea4772
9b97412d3aa791870016ab3c6f565b6a6afa1764f98e969833aec093b9b29919