From a9e1d96cd8d8105f878e60065820f3316a2cd410 Mon Sep 17 00:00:00 2001 From: stephan Date: Fri, 23 Dec 2022 18:19:28 +0000 Subject: [PATCH] Cherry-pick [c4dab53b8ea3401abd] for sqlite3.wasm.xWrap() optimizations. FossilOrigin-Name: 9b97412d3aa791870016ab3c6f565b6a6afa1764f98e969833aec093b9b29919 --- ext/wasm/common/whwasmutil.js | 205 +++++++++++++++++++--------------- ext/wasm/tester1.c-pp.js | 64 ++++++++++- manifest | 17 +-- manifest.uuid | 2 +- 4 files changed, 184 insertions(+), 104 deletions(-) diff --git a/ext/wasm/common/whwasmutil.js b/ext/wasm/common/whwasmutil.js index 87cc100f33..ecd08327f4 100644 --- a/ext/wasm/common/whwasmutil.js +++ b/ext/wasm/common/whwasmutil.js @@ -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||[]); }; /** diff --git a/ext/wasm/tester1.c-pp.js b/ext/wasm/tester1.c-pp.js index bef34a0ba5..eb014d7b51 100644 --- a/ext/wasm/tester1.c-pp.js +++ b/ext/wasm/tester1.c-pp.js @@ -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*/ diff --git a/manifest b/manifest index 9863dbab45..bc2050511c 100644 --- a/manifest +++ b/manifest @@ -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. diff --git a/manifest.uuid b/manifest.uuid index f42d26d9c5..a270ba7305 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -c14bbe1606c1450b709970f922b94a641dfc8f9bd09126501d7dc4db99ea4772 \ No newline at end of file +9b97412d3aa791870016ab3c6f565b6a6afa1764f98e969833aec093b9b29919 \ No newline at end of file