1/** 2 2022-07-08 3 4 The author disclaims copyright to this source code. In place of a 5 legal notice, here is a blessing: 6 7 * May you do good and not evil. 8 * May you find forgiveness for yourself and forgive others. 9 * May you share freely, never taking more than you give. 10 11 *********************************************************************** 12 13 The whwasmutil is developed in conjunction with the Jaccwabyt 14 project: 15 16 https://fossil.wanderinghorse.net/r/jaccwabyt 17 18 Maintenance reminder: If you're reading this in a tree other than 19 the Jaccwabyt tree, note that this copy may be replaced with 20 upstream copies of that one from time to time. Thus the code 21 installed by this function "should not" be edited outside of that 22 project, else it risks getting overwritten. 23*/ 24/** 25 This function is intended to simplify porting around various bits 26 of WASM-related utility code from project to project. 27 28 The primary goal of this code is to replace, where possible, 29 Emscripten-generated glue code with equivalent utility code which 30 can be used in arbitrary WASM environments built with toolchains 31 other than Emscripten. As of this writing, this code is capable of 32 acting as a replacement for Emscripten's generated glue code 33 _except_ that the latter installs handlers for Emscripten-provided 34 APIs such as its "FS" (virtual filesystem) API. Loading of such 35 things still requires using Emscripten's glue, but the post-load 36 utility APIs provided by this code are still usable as replacements 37 for their sub-optimally-documented Emscripten counterparts. 38 39 Intended usage: 40 41 ``` 42 self.WhWasmUtilInstaller(appObject); 43 delete self.WhWasmUtilInstaller; 44 ``` 45 46 Its global-scope symbol is intended only to provide an easy way to 47 make it available to 3rd-party scripts and "should" be deleted 48 after calling it. That symbols is _not_ used within the library. 49 50 Forewarning: this API explicitly targets only browser 51 environments. If a given non-browser environment has the 52 capabilities needed for a given feature (e.g. TextEncoder), great, 53 but it does not go out of its way to account for them and does not 54 provide compatibility crutches for them. 55 56 It currently offers alternatives to the following 57 Emscripten-generated APIs: 58 59 - OPTIONALLY memory allocation, but how this gets imported is 60 environment-specific. Most of the following features only work 61 if allocation is available. 62 63 - WASM-exported "indirect function table" access and 64 manipulation. e.g. creating new WASM-side functions using JS 65 functions, analog to Emscripten's addFunction() and 66 removeFunction() but slightly different. 67 68 - Get/set specific heap memory values, analog to Emscripten's 69 getValue() and setValue(). 70 71 - String length counting in UTF-8 bytes (C-style and JS strings). 72 73 - JS string to C-string conversion and vice versa, analog to 74 Emscripten's stringToUTF8Array() and friends, but with slighter 75 different interfaces. 76 77 - JS string to Uint8Array conversion, noting that browsers actually 78 already have this built in via TextEncoder. 79 80 - "Scoped" allocation, such that allocations made inside of a given 81 explicit scope will be automatically cleaned up when the scope is 82 closed. This is fundamentally similar to Emscripten's 83 stackAlloc() and friends but uses the heap instead of the stack 84 because access to the stack requires C code. 85 86 - Create JS wrappers for WASM functions, analog to Emscripten's 87 ccall() and cwrap() functions, except that the automatic 88 conversions for function arguments and return values can be 89 easily customized by the client by assigning custom function 90 signature type names to conversion functions. Essentially, 91 it's ccall() and cwrap() on steroids. 92 93 How to install... 94 95 Passing an object to this function will install the functionality 96 into that object. Afterwards, client code "should" delete the global 97 symbol. 98 99 This code requires that the target object have the following 100 properties, noting that they needn't be available until the first 101 time one of the installed APIs is used (as opposed to when this 102 function is called) except where explicitly noted: 103 104 - `exports` must be a property of the target object OR a property 105 of `target.instance` (a WebAssembly.Module instance) and it must 106 contain the symbols exported by the WASM module associated with 107 this code. In an Enscripten environment it must be set to 108 `Module['asm']`. The exports object must contain a minimum of the 109 following symbols: 110 111 - `memory`: a WebAssembly.Memory object representing the WASM 112 memory. _Alternately_, the `memory` property can be set on the 113 target instance, in particular if the WASM heap memory is 114 initialized in JS an _imported_ into WASM, as opposed to being 115 initialized in WASM and exported to JS. 116 117 - `__indirect_function_table`: the WebAssembly.Table object which 118 holds WASM-exported functions. This API does not strictly 119 require that the table be able to grow but it will throw if its 120 `installFunction()` is called and the table cannot grow. 121 122 In order to simplify downstream usage, if `target.exports` is not 123 set when this is called then a property access interceptor 124 (read-only, configurable, enumerable) gets installed as `exports` 125 which resolves to `target.instance.exports`, noting that the latter 126 property need not exist until the first time `target.exports` is 127 accessed. 128 129 Some APIs _optionally_ make use of the `bigIntEnabled` property of 130 the target object. It "should" be set to true if the WASM 131 environment is compiled with BigInt support, else it must be 132 false. If it is false, certain BigInt-related features will trigger 133 an exception if invoked. This property, if not set when this is 134 called, will get a default value of true only if the BigInt64Array 135 constructor is available, else it will default to false. 136 137 Some optional APIs require that the target have the following 138 methods: 139 140 - 'alloc()` must behave like C's `malloc()`, allocating N bytes of 141 memory and returning its pointer. In Emscripten this is 142 conventionally made available via `Module['_malloc']`. This API 143 requires that the alloc routine throw on allocation error, as 144 opposed to returning null or 0. 145 146 - 'dealloc()` must behave like C's `free()`, accepting either a 147 pointer returned from its allocation counterpart or the values 148 null/0 (for which it must be a no-op). allocating N bytes of 149 memory and returning its pointer. In Emscripten this is 150 conventionally made available via `Module['_free']`. 151 152 APIs which require allocation routines are explicitly documented as 153 such and/or have "alloc" in their names. 154 155 This code is developed and maintained in conjunction with the 156 Jaccwabyt project: 157 158 https://fossil.wanderinghorse.net/r/jaccwabbyt 159 160 More specifically: 161 162 https://fossil.wanderinghorse.net/r/jaccwabbyt/file/common/whwasmutil.js 163*/ 164self.WhWasmUtilInstaller = function(target){ 165 'use strict'; 166 if(undefined===target.bigIntEnabled){ 167 target.bigIntEnabled = !!self['BigInt64Array']; 168 } 169 170 /** Throws a new Error, the message of which is the concatenation of 171 all args with a space between each. */ 172 const toss = (...args)=>{throw new Error(args.join(' '))}; 173 174 if(!target.exports){ 175 Object.defineProperty(target, 'exports', { 176 enumerable: true, configurable: true, 177 get: ()=>(target.instance && target.instance.exports) 178 }); 179 } 180 181 /********* 182 alloc()/dealloc() auto-install... 183 184 This would be convenient but it can also cause us to pick up 185 malloc() even when the client code is using a different exported 186 allocator (who, me?), which is bad. malloc() may be exported even 187 if we're not explicitly using it and overriding the malloc() 188 function, linking ours first, is not always feasible when using a 189 malloc() proxy, as it can lead to recursion and stack overflow 190 (who, me?). So... we really need the downstream code to set up 191 target.alloc/dealloc() itself. 192 ******/ 193 /****** 194 if(target.exports){ 195 //Maybe auto-install alloc()/dealloc()... 196 if(!target.alloc && target.exports.malloc){ 197 target.alloc = function(n){ 198 const m = this(n); 199 return m || toss("Allocation of",n,"byte(s) failed."); 200 }.bind(target.exports.malloc); 201 } 202 203 if(!target.dealloc && target.exports.free){ 204 target.dealloc = function(ptr){ 205 if(ptr) this(ptr); 206 }.bind(target.exports.free); 207 } 208 }*******/ 209 210 /** 211 Pointers in WASM are currently assumed to be 32-bit, but someday 212 that will certainly change. 213 */ 214 const ptrIR = target.pointerIR || 'i32'; 215 const ptrSizeof = ('i32'===ptrIR ? 4 216 : ('i64'===ptrIR 217 ? 8 : toss("Unhandled ptrSizeof:",ptrIR))); 218 /** Stores various cached state. */ 219 const cache = Object.create(null); 220 /** Previously-recorded size of cache.memory.buffer, noted so that 221 we can recreate the view objects if the heap grows. */ 222 cache.heapSize = 0; 223 /** WebAssembly.Memory object extracted from target.memory or 224 target.exports.memory the first time heapWrappers() is 225 called. */ 226 cache.memory = null; 227 /** uninstallFunction() puts table indexes in here for reuse and 228 installFunction() extracts them. */ 229 cache.freeFuncIndexes = []; 230 /** 231 Used by scopedAlloc() and friends. 232 */ 233 cache.scopedAlloc = []; 234 235 cache.utf8Decoder = new TextDecoder(); 236 cache.utf8Encoder = new TextEncoder('utf-8'); 237 238 /** 239 If (cache.heapSize !== cache.memory.buffer.byteLength), i.e. if 240 the heap has grown since the last call, updates cache.HEAPxyz. 241 Returns the cache object. 242 */ 243 const heapWrappers = function(){ 244 if(!cache.memory){ 245 cache.memory = (target.memory instanceof WebAssembly.Memory) 246 ? target.memory : target.exports.memory; 247 }else if(cache.heapSize === cache.memory.buffer.byteLength){ 248 return cache; 249 } 250 // heap is newly-acquired or has been resized.... 251 const b = cache.memory.buffer; 252 cache.HEAP8 = new Int8Array(b); cache.HEAP8U = new Uint8Array(b); 253 cache.HEAP16 = new Int16Array(b); cache.HEAP16U = new Uint16Array(b); 254 cache.HEAP32 = new Int32Array(b); cache.HEAP32U = new Uint32Array(b); 255 if(target.bigIntEnabled){ 256 cache.HEAP64 = new BigInt64Array(b); cache.HEAP64U = new BigUint64Array(b); 257 } 258 cache.HEAP32F = new Float32Array(b); cache.HEAP64F = new Float64Array(b); 259 cache.heapSize = b.byteLength; 260 return cache; 261 }; 262 263 /** Convenience equivalent of this.heapForSize(8,false). */ 264 target.heap8 = ()=>heapWrappers().HEAP8; 265 266 /** Convenience equivalent of this.heapForSize(8,true). */ 267 target.heap8u = ()=>heapWrappers().HEAP8U; 268 269 /** Convenience equivalent of this.heapForSize(16,false). */ 270 target.heap16 = ()=>heapWrappers().HEAP16; 271 272 /** Convenience equivalent of this.heapForSize(16,true). */ 273 target.heap16u = ()=>heapWrappers().HEAP16U; 274 275 /** Convenience equivalent of this.heapForSize(32,false). */ 276 target.heap32 = ()=>heapWrappers().HEAP32; 277 278 /** Convenience equivalent of this.heapForSize(32,true). */ 279 target.heap32u = ()=>heapWrappers().HEAP32U; 280 281 /** 282 Requires n to be one of: 283 284 - integer 8, 16, or 32. 285 - A integer-type TypedArray constructor: Int8Array, Int16Array, 286 Int32Array, or their Uint counterparts. 287 288 If this.bigIntEnabled is true, it also accepts the value 64 or a 289 BigInt64Array/BigUint64Array, else it throws if passed 64 or one 290 of those constructors. 291 292 Returns an integer-based TypedArray view of the WASM heap 293 memory buffer associated with the given block size. If passed 294 an integer as the first argument and unsigned is truthy then 295 the "U" (unsigned) variant of that view is returned, else the 296 signed variant is returned. If passed a TypedArray value, the 297 2nd argument is ignores. Note that Float32Array and 298 Float64Array views are not supported by this function. 299 300 Note that growth of the heap will invalidate any references to 301 this heap, so do not hold a reference longer than needed and do 302 not use a reference after any operation which may 303 allocate. Instead, re-fetch the reference by calling this 304 function again. 305 306 Throws if passed an invalid n. 307 308 Pedantic side note: the name "heap" is a bit of a misnomer. In an 309 Emscripten environment, the memory managed via the stack 310 allocation API is in the same Memory object as the heap (which 311 makes sense because otherwise arbitrary pointer X would be 312 ambiguous: is it in the heap or the stack?). 313 */ 314 target.heapForSize = function(n,unsigned = false){ 315 let ctor; 316 const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength) 317 ? cache : heapWrappers(); 318 switch(n){ 319 case Int8Array: return c.HEAP8; case Uint8Array: return c.HEAP8U; 320 case Int16Array: return c.HEAP16; case Uint16Array: return c.HEAP16U; 321 case Int32Array: return c.HEAP32; case Uint32Array: return c.HEAP32U; 322 case 8: return unsigned ? c.HEAP8U : c.HEAP8; 323 case 16: return unsigned ? c.HEAP16U : c.HEAP16; 324 case 32: return unsigned ? c.HEAP32U : c.HEAP32; 325 case 64: 326 if(c.HEAP64) return unsigned ? c.HEAP64U : c.HEAP64; 327 break; 328 default: 329 if(this.bigIntEnabled){ 330 if(n===self['BigUint64Array']) return c.HEAP64U; 331 else if(n===self['BigInt64Array']) return c.HEAP64; 332 break; 333 } 334 } 335 toss("Invalid heapForSize() size: expecting 8, 16, 32,", 336 "or (if BigInt is enabled) 64."); 337 }.bind(target); 338 339 /** 340 Returns the WASM-exported "indirect function table." 341 */ 342 target.functionTable = function(){ 343 return target.exports.__indirect_function_table; 344 /** -----------------^^^^^ "seems" to be a standardized export name. 345 From Emscripten release notes from 2020-09-10: 346 - Use `__indirect_function_table` as the import name for the 347 table, which is what LLVM does. 348 */ 349 }.bind(target); 350 351 /** 352 Given a function pointer, returns the WASM function table entry 353 if found, else returns a falsy value. 354 */ 355 target.functionEntry = function(fptr){ 356 const ft = this.functionTable(); 357 return fptr < ft.length ? ft.get(fptr) : undefined; 358 }.bind(target); 359 360 /** 361 Creates a WASM function which wraps the given JS function and 362 returns the JS binding of that WASM function. The signature 363 argument must be the Jaccwabyt-format or Emscripten 364 addFunction()-format function signature string. In short: in may 365 have one of the following formats: 366 367 - Emscripten: `x...`, where the first x is a letter representing 368 the result type and subsequent letters represent the argument 369 types. See below. 370 371 - Jaccwabyt: `x(...)` where `x` is the letter representing the 372 result type and letters in the parens (if any) represent the 373 argument types. See below. 374 375 Supported letters: 376 377 - `i` = int32 378 - `p` = int32 ("pointer") 379 - `j` = int64 380 - `f` = float32 381 - `d` = float64 382 - `v` = void, only legal for use as the result type 383 384 It throws if an invalid signature letter is used. 385 386 Jaccwabyt-format signatures support some additional letters which 387 have no special meaning here but (in this context) act as aliases 388 for other letters: 389 390 - `s`, `P`: same as `p` 391 392 Sidebar: this code is developed together with Jaccwabyt, thus the 393 support for its signature format. 394 */ 395 target.jsFuncToWasm = function f(func, sig){ 396 /** Attribution: adapted up from Emscripten-generated glue code, 397 refactored primarily for efficiency's sake, eliminating 398 call-local functions and superfluous temporary arrays. */ 399 if(!f._){/*static init...*/ 400 f._ = { 401 // Map of signature letters to type IR values 402 sigTypes: Object.create(null), 403 // Map of type IR values to WASM type code values 404 typeCodes: Object.create(null), 405 /** Encodes n, which must be <2^14 (16384), into target array 406 tgt, as a little-endian value, using the given method 407 ('push' or 'unshift'). */ 408 uleb128Encode: function(tgt, method, n){ 409 if(n<128) tgt[method](n); 410 else tgt[method]( (n % 128) | 128, n>>7); 411 }, 412 /** Intentionally-lax pattern for Jaccwabyt-format function 413 pointer signatures, the intent of which is simply to 414 distinguish them from Emscripten-format signatures. The 415 downstream checks are less lax. */ 416 rxJSig: /^(\w)\((\w*)\)$/, 417 /** Returns the parameter-value part of the given signature 418 string. */ 419 sigParams: function(sig){ 420 const m = f._.rxJSig.exec(sig); 421 return m ? m[2] : sig.substr(1); 422 }, 423 /** Returns the IR value for the given letter or throws 424 if the letter is invalid. */ 425 letterType: (x)=>f._.sigTypes[x] || toss("Invalid signature letter:",x), 426 /** Returns an object describing the result type and parameter 427 type(s) of the given function signature, or throws if the 428 signature is invalid. */ 429 /******** // only valid for use with the WebAssembly.Function ctor, which 430 // is not yet documented on MDN. 431 sigToWasm: function(sig){ 432 const rc = {parameters:[], results: []}; 433 if('v'!==sig[0]) rc.results.push(f._.letterType(sig[0])); 434 for(const x of f._.sigParams(sig)){ 435 rc.parameters.push(f._.letterType(x)); 436 } 437 return rc; 438 },************/ 439 /** Pushes the WASM data type code for the given signature 440 letter to the given target array. Throws if letter is 441 invalid. */ 442 pushSigType: (dest, letter)=>dest.push(f._.typeCodes[f._.letterType(letter)]) 443 }; 444 f._.sigTypes.i = f._.sigTypes.p = f._.sigTypes.P = f._.sigTypes.s = 'i32'; 445 f._.sigTypes.j = 'i64'; f._.sigTypes.f = 'f32'; f._.sigTypes.d = 'f64'; 446 f._.typeCodes['i32'] = 0x7f; f._.typeCodes['i64'] = 0x7e; 447 f._.typeCodes['f32'] = 0x7d; f._.typeCodes['f64'] = 0x7c; 448 }/*static init*/ 449 const sigParams = f._.sigParams(sig); 450 const wasmCode = [0x01/*count: 1*/, 0x60/*function*/]; 451 f._.uleb128Encode(wasmCode, 'push', sigParams.length); 452 for(const x of sigParams) f._.pushSigType(wasmCode, x); 453 if('v'===sig[0]) wasmCode.push(0); 454 else{ 455 wasmCode.push(1); 456 f._.pushSigType(wasmCode, sig[0]); 457 } 458 f._.uleb128Encode(wasmCode, 'unshift', wasmCode.length)/* type section length */; 459 wasmCode.unshift( 460 0x00, 0x61, 0x73, 0x6d, /* magic: "\0asm" */ 461 0x01, 0x00, 0x00, 0x00, /* version: 1 */ 462 0x01 /* type section code */ 463 ); 464 wasmCode.push( 465 /* import section: */ 0x02, 0x07, 466 /* (import "e" "f" (func 0 (type 0))): */ 467 0x01, 0x01, 0x65, 0x01, 0x66, 0x00, 0x00, 468 /* export section: */ 0x07, 0x05, 469 /* (export "f" (func 0 (type 0))): */ 470 0x01, 0x01, 0x66, 0x00, 0x00 471 ); 472 return (new WebAssembly.Instance( 473 new WebAssembly.Module(new Uint8Array(wasmCode)), { 474 e: { f: func } 475 })).exports['f']; 476 }/*jsFuncToWasm()*/; 477 478 /** 479 Expects a JS function and signature, exactly as for 480 this.jsFuncToWasm(). It uses that function to create a 481 WASM-exported function, installs that function to the next 482 available slot of this.functionTable(), and returns the 483 function's index in that table (which acts as a pointer to that 484 function). The returned pointer can be passed to 485 removeFunction() to uninstall it and free up the table slot for 486 reuse. 487 488 As a special case, if the passed-in function is a WASM-exported 489 function then the signature argument is ignored and func is 490 installed as-is, without requiring re-compilation/re-wrapping. 491 492 This function will propagate an exception if 493 WebAssembly.Table.grow() throws or this.jsFuncToWasm() throws. 494 The former case can happen in an Emscripten-compiled 495 environment when building without Emscripten's 496 `-sALLOW_TABLE_GROWTH` flag. 497 498 Sidebar: this function differs from Emscripten's addFunction() 499 _primarily_ in that it does not share that function's 500 undocumented behavior of reusing a function if it's passed to 501 addFunction() more than once, which leads to removeFunction() 502 breaking clients which do not take care to avoid that case: 503 504 https://github.com/emscripten-core/emscripten/issues/17323 505 */ 506 target.installFunction = function f(func, sig){ 507 const ft = this.functionTable(); 508 const oldLen = ft.length; 509 let ptr; 510 while(cache.freeFuncIndexes.length){ 511 ptr = cache.freeFuncIndexes.pop(); 512 if(ft.get(ptr)){ /* Table was modified via a different API */ 513 ptr = null; 514 continue; 515 }else{ 516 break; 517 } 518 } 519 if(!ptr){ 520 ptr = oldLen; 521 ft.grow(1); 522 } 523 try{ 524 /*this will only work if func is a WASM-exported function*/ 525 ft.set(ptr, func); 526 return ptr; 527 }catch(e){ 528 if(!(e instanceof TypeError)){ 529 if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen); 530 throw e; 531 } 532 } 533 // It's not a WASM-exported function, so compile one... 534 try { 535 ft.set(ptr, this.jsFuncToWasm(func, sig)); 536 }catch(e){ 537 if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen); 538 throw e; 539 } 540 return ptr; 541 }.bind(target); 542 543 /** 544 Requires a pointer value previously returned from 545 this.installFunction(). Removes that function from the WASM 546 function table, marks its table slot as free for re-use, and 547 returns that function. It is illegal to call this before 548 installFunction() has been called and results are undefined if 549 ptr was not returned by that function. The returned function 550 may be passed back to installFunction() to reinstall it. 551 */ 552 target.uninstallFunction = function(ptr){ 553 const fi = cache.freeFuncIndexes; 554 const ft = this.functionTable(); 555 fi.push(ptr); 556 const rc = ft.get(ptr); 557 ft.set(ptr, null); 558 return rc; 559 }.bind(target); 560 561 /** 562 Given a WASM heap memory address and a data type name in the form 563 (i8, i16, i32, i64, float (or f32), double (or f64)), this 564 fetches the numeric value from that address and returns it as a 565 number or, for the case of type='i64', a BigInt (noting that that 566 type triggers an exception if this.bigIntEnabled is 567 falsy). Throws if given an invalid type. 568 569 As a special case, if type ends with a `*`, it is considered to 570 be a pointer type and is treated as the WASM numeric type 571 appropriate for the pointer size (`i32`). 572 573 While likely not obvious, this routine and its setMemValue() 574 counterpart are how pointer-to-value _output_ parameters 575 in WASM-compiled C code can be interacted with: 576 577 ``` 578 const ptr = alloc(4); 579 setMemValue(ptr, 0, 'i32'); // clear the ptr's value 580 aCFuncWithOutputPtrToInt32Arg( ptr ); // e.g. void foo(int *x); 581 const result = getMemValue(ptr, 'i32'); // fetch ptr's value 582 dealloc(ptr); 583 ``` 584 585 scopedAlloc() and friends can be used to make handling of 586 `ptr` safe against leaks in the case of an exception: 587 588 ``` 589 let result; 590 const scope = scopedAllocPush(); 591 try{ 592 const ptr = scopedAlloc(4); 593 setMemValue(ptr, 0, 'i32'); 594 aCFuncWithOutputPtrArg( ptr ); 595 result = getMemValue(ptr, 'i32'); 596 }finally{ 597 scopedAllocPop(scope); 598 } 599 ``` 600 601 As a rule setMemValue() must be called to set (typically zero 602 out) the pointer's value, else it will contain an essentially 603 random value. 604 605 See: setMemValue() 606 */ 607 target.getMemValue = function(ptr, type='i8'){ 608 if(type.endsWith('*')) type = ptrIR; 609 const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength) 610 ? cache : heapWrappers(); 611 switch(type){ 612 case 'i1': 613 case 'i8': return c.HEAP8[ptr>>0]; 614 case 'i16': return c.HEAP16[ptr>>1]; 615 case 'i32': return c.HEAP32[ptr>>2]; 616 case 'i64': 617 if(this.bigIntEnabled) return BigInt(c.HEAP64[ptr>>3]); 618 break; 619 case 'float': case 'f32': return c.HEAP32F[ptr>>2]; 620 case 'double': case 'f64': return Number(c.HEAP64F[ptr>>3]); 621 default: break; 622 } 623 toss('Invalid type for getMemValue():',type); 624 }.bind(target); 625 626 /** 627 The counterpart of getMemValue(), this sets a numeric value at 628 the given WASM heap address, using the type to define how many 629 bytes are written. Throws if given an invalid type. See 630 getMemValue() for details about the type argument. If the 3rd 631 argument ends with `*` then it is treated as a pointer type and 632 this function behaves as if the 3rd argument were `i32`. 633 634 This function returns itself. 635 */ 636 target.setMemValue = function f(ptr, value, type='i8'){ 637 if (type.endsWith('*')) type = ptrIR; 638 const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength) 639 ? cache : heapWrappers(); 640 switch (type) { 641 case 'i1': 642 case 'i8': c.HEAP8[ptr>>0] = value; return f; 643 case 'i16': c.HEAP16[ptr>>1] = value; return f; 644 case 'i32': c.HEAP32[ptr>>2] = value; return f; 645 case 'i64': 646 if(c.HEAP64){ 647 c.HEAP64[ptr>>3] = BigInt(value); 648 return f; 649 } 650 break; 651 case 'float': case 'f32': c.HEAP32F[ptr>>2] = value; return f; 652 case 'double': case 'f64': c.HEAP64F[ptr>>3] = value; return f; 653 } 654 toss('Invalid type for setMemValue(): ' + type); 655 }; 656 657 /** 658 Expects ptr to be a pointer into the WASM heap memory which 659 refers to a NUL-terminated C-style string encoded as UTF-8. 660 Returns the length, in bytes, of the string, as for `strlen(3)`. 661 As a special case, if !ptr then it it returns `null`. Throws if 662 ptr is out of range for target.heap8u(). 663 */ 664 target.cstrlen = function(ptr){ 665 if(!ptr) return null; 666 const h = heapWrappers().HEAP8U; 667 let pos = ptr; 668 for( ; h[pos] !== 0; ++pos ){} 669 return pos - ptr; 670 }; 671 672 /** Internal helper to use in operations which need to distinguish 673 between SharedArrayBuffer heap memory and non-shared heap. */ 674 const __SAB = ('undefined'===typeof SharedArrayBuffer) 675 ? function(){} : SharedArrayBuffer; 676 const __utf8Decode = function(arrayBuffer, begin, end){ 677 return cache.utf8Decoder.decode( 678 (arrayBuffer.buffer instanceof __SAB) 679 ? arrayBuffer.slice(begin, end) 680 : arrayBuffer.subarray(begin, end) 681 ); 682 }; 683 684 /** 685 Expects ptr to be a pointer into the WASM heap memory which 686 refers to a NUL-terminated C-style string encoded as UTF-8. This 687 function counts its byte length using cstrlen() then returns a 688 JS-format string representing its contents. As a special case, if 689 ptr is falsy, `null` is returned. 690 */ 691 target.cstringToJs = function(ptr){ 692 const n = this.cstrlen(ptr); 693 return n ? __utf8Decode(heapWrappers().HEAP8U, ptr, ptr+n) : (null===n ? n : ""); 694 }.bind(target); 695 696 /** 697 Given a JS string, this function returns its UTF-8 length in 698 bytes. Returns null if str is not a string. 699 */ 700 target.jstrlen = function(str){ 701 /** Attribution: derived from Emscripten's lengthBytesUTF8() */ 702 if('string'!==typeof str) return null; 703 const n = str.length; 704 let len = 0; 705 for(let i = 0; i < n; ++i){ 706 let u = str.charCodeAt(i); 707 if(u>=0xd800 && u<=0xdfff){ 708 u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); 709 } 710 if(u<=0x7f) ++len; 711 else if(u<=0x7ff) len += 2; 712 else if(u<=0xffff) len += 3; 713 else len += 4; 714 } 715 return len; 716 }; 717 718 /** 719 Encodes the given JS string as UTF8 into the given TypedArray 720 tgt, starting at the given offset and writing, at most, maxBytes 721 bytes (including the NUL terminator if addNul is true, else no 722 NUL is added). If it writes any bytes at all and addNul is true, 723 it always NUL-terminates the output, even if doing so means that 724 the NUL byte is all that it writes. 725 726 If maxBytes is negative (the default) then it is treated as the 727 remaining length of tgt, starting at the given offset. 728 729 If writing the last character would surpass the maxBytes count 730 because the character is multi-byte, that character will not be 731 written (as opposed to writing a truncated multi-byte character). 732 This can lead to it writing as many as 3 fewer bytes than 733 maxBytes specifies. 734 735 Returns the number of bytes written to the target, _including_ 736 the NUL terminator (if any). If it returns 0, it wrote nothing at 737 all, which can happen if: 738 739 - str is empty and addNul is false. 740 - offset < 0. 741 - maxBytes == 0. 742 - maxBytes is less than the byte length of a multi-byte str[0]. 743 744 Throws if tgt is not an Int8Array or Uint8Array. 745 746 Design notes: 747 748 - In C's strcpy(), the destination pointer is the first 749 argument. That is not the case here primarily because the 3rd+ 750 arguments are all referring to the destination, so it seems to 751 make sense to have them grouped with it. 752 753 - Emscripten's counterpart of this function (stringToUTF8Array()) 754 returns the number of bytes written sans NUL terminator. That 755 is, however, ambiguous: str.length===0 or maxBytes===(0 or 1) 756 all cause 0 to be returned. 757 */ 758 target.jstrcpy = function(jstr, tgt, offset = 0, maxBytes = -1, addNul = true){ 759 /** Attribution: the encoding bits are taken from Emscripten's 760 stringToUTF8Array(). */ 761 if(!tgt || (!(tgt instanceof Int8Array) && !(tgt instanceof Uint8Array))){ 762 toss("jstrcpy() target must be an Int8Array or Uint8Array."); 763 } 764 if(maxBytes<0) maxBytes = tgt.length - offset; 765 if(!(maxBytes>0) || !(offset>=0)) return 0; 766 let i = 0, max = jstr.length; 767 const begin = offset, end = offset + maxBytes - (addNul ? 1 : 0); 768 for(; i < max && offset < end; ++i){ 769 let u = jstr.charCodeAt(i); 770 if(u>=0xd800 && u<=0xdfff){ 771 u = 0x10000 + ((u & 0x3FF) << 10) | (jstr.charCodeAt(++i) & 0x3FF); 772 } 773 if(u<=0x7f){ 774 if(offset >= end) break; 775 tgt[offset++] = u; 776 }else if(u<=0x7ff){ 777 if(offset + 1 >= end) break; 778 tgt[offset++] = 0xC0 | (u >> 6); 779 tgt[offset++] = 0x80 | (u & 0x3f); 780 }else if(u<=0xffff){ 781 if(offset + 2 >= end) break; 782 tgt[offset++] = 0xe0 | (u >> 12); 783 tgt[offset++] = 0x80 | ((u >> 6) & 0x3f); 784 tgt[offset++] = 0x80 | (u & 0x3f); 785 }else{ 786 if(offset + 3 >= end) break; 787 tgt[offset++] = 0xf0 | (u >> 18); 788 tgt[offset++] = 0x80 | ((u >> 12) & 0x3f); 789 tgt[offset++] = 0x80 | ((u >> 6) & 0x3f); 790 tgt[offset++] = 0x80 | (u & 0x3f); 791 } 792 } 793 if(addNul) tgt[offset++] = 0; 794 return offset - begin; 795 }; 796 797 /** 798 Works similarly to C's strncpy(), copying, at most, n bytes (not 799 characters) from srcPtr to tgtPtr. It copies until n bytes have 800 been copied or a 0 byte is reached in src. _Unlike_ strncpy(), it 801 returns the number of bytes it assigns in tgtPtr, _including_ the 802 NUL byte (if any). If n is reached before a NUL byte in srcPtr, 803 tgtPtr will _not_ be NULL-terminated. If a NUL byte is reached 804 before n bytes are copied, tgtPtr will be NUL-terminated. 805 806 If n is negative, cstrlen(srcPtr)+1 is used to calculate it, the 807 +1 being for the NUL byte. 808 809 Throws if tgtPtr or srcPtr are falsy. Results are undefined if: 810 811 - either is not a pointer into the WASM heap or 812 813 - srcPtr is not NUL-terminated AND n is less than srcPtr's 814 logical length. 815 816 ACHTUNG: it is possible to copy partial multi-byte characters 817 this way, and converting such strings back to JS strings will 818 have undefined results. 819 */ 820 target.cstrncpy = function(tgtPtr, srcPtr, n){ 821 if(!tgtPtr || !srcPtr) toss("cstrncpy() does not accept NULL strings."); 822 if(n<0) n = this.cstrlen(strPtr)+1; 823 else if(!(n>0)) return 0; 824 const heap = this.heap8u(); 825 let i = 0, ch; 826 for(; i < n && (ch = heap[srcPtr+i]); ++i){ 827 heap[tgtPtr+i] = ch; 828 } 829 if(i<n) heap[tgtPtr + i++] = 0; 830 return i; 831 }.bind(target); 832 833 /** 834 For the given JS string, returns a Uint8Array of its contents 835 encoded as UTF-8. If addNul is true, the returned array will have 836 a trailing 0 entry, else it will not. 837 */ 838 target.jstrToUintArray = (str, addNul=false)=>{ 839 return cache.utf8Encoder.encode(addNul ? (str+"\0") : str); 840 // Or the hard way... 841 /** Attribution: derived from Emscripten's stringToUTF8Array() */ 842 //const a = [], max = str.length; 843 //let i = 0, pos = 0; 844 //for(; i < max; ++i){ 845 // let u = str.charCodeAt(i); 846 // if(u>=0xd800 && u<=0xdfff){ 847 // u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); 848 // } 849 // if(u<=0x7f) a[pos++] = u; 850 // else if(u<=0x7ff){ 851 // a[pos++] = 0xC0 | (u >> 6); 852 // a[pos++] = 0x80 | (u & 63); 853 // }else if(u<=0xffff){ 854 // a[pos++] = 0xe0 | (u >> 12); 855 // a[pos++] = 0x80 | ((u >> 6) & 63); 856 // a[pos++] = 0x80 | (u & 63); 857 // }else{ 858 // a[pos++] = 0xf0 | (u >> 18); 859 // a[pos++] = 0x80 | ((u >> 12) & 63); 860 // a[pos++] = 0x80 | ((u >> 6) & 63); 861 // a[pos++] = 0x80 | (u & 63); 862 // } 863 // } 864 // return new Uint8Array(a); 865 }; 866 867 const __affirmAlloc = (obj,funcName)=>{ 868 if(!(obj.alloc instanceof Function) || 869 !(obj.dealloc instanceof Function)){ 870 toss("Object is missing alloc() and/or dealloc() function(s)", 871 "required by",funcName+"()."); 872 } 873 }; 874 875 const __allocCStr = function(jstr, returnWithLength, allocator, funcName){ 876 __affirmAlloc(this, funcName); 877 if('string'!==typeof jstr) return null; 878 const n = this.jstrlen(jstr), 879 ptr = allocator(n+1); 880 this.jstrcpy(jstr, this.heap8u(), ptr, n+1, true); 881 return returnWithLength ? [ptr, n] : ptr; 882 }.bind(target); 883 884 /** 885 Uses target.alloc() to allocate enough memory for jstrlen(jstr)+1 886 bytes of memory, copies jstr to that memory using jstrcpy(), 887 NUL-terminates it, and returns the pointer to that C-string. 888 Ownership of the pointer is transfered to the caller, who must 889 eventually pass the pointer to dealloc() to free it. 890 891 If passed a truthy 2nd argument then its return semantics change: 892 it returns [ptr,n], where ptr is the C-string's pointer and n is 893 its cstrlen(). 894 895 Throws if `target.alloc` or `target.dealloc` are not functions. 896 */ 897 target.allocCString = 898 (jstr, returnWithLength=false)=>__allocCStr(jstr, returnWithLength, 899 target.alloc, 'allocCString()'); 900 901 /** 902 Starts an "allocation scope." All allocations made using 903 scopedAlloc() are recorded in this scope and are freed when the 904 value returned from this function is passed to 905 scopedAllocPop(). 906 907 This family of functions requires that the API's object have both 908 `alloc()` and `dealloc()` methods, else this function will throw. 909 910 Intended usage: 911 912 ``` 913 const scope = scopedAllocPush(); 914 try { 915 const ptr1 = scopedAlloc(100); 916 const ptr2 = scopedAlloc(200); 917 const ptr3 = scopedAlloc(300); 918 ... 919 // Note that only allocations made via scopedAlloc() 920 // are managed by this allocation scope. 921 }finally{ 922 scopedAllocPop(scope); 923 } 924 ``` 925 926 The value returned by this function must be treated as opaque by 927 the caller, suitable _only_ for passing to scopedAllocPop(). 928 Its type and value are not part of this function's API and may 929 change in any given version of this code. 930 931 `scopedAlloc.level` can be used to determine how many scoped 932 alloc levels are currently active. 933 */ 934 target.scopedAllocPush = function(){ 935 __affirmAlloc(this, 'scopedAllocPush'); 936 const a = []; 937 cache.scopedAlloc.push(a); 938 return a; 939 }.bind(target); 940 941 /** 942 Cleans up all allocations made using scopedAlloc() in the context 943 of the given opaque state object, which must be a value returned 944 by scopedAllocPush(). See that function for an example of how to 945 use this function. 946 947 Though scoped allocations are managed like a stack, this API 948 behaves properly if allocation scopes are popped in an order 949 other than the order they were pushed. 950 951 If called with no arguments, it pops the most recent 952 scopedAllocPush() result: 953 954 ``` 955 scopedAllocPush(); 956 try{ ... } finally { scopedAllocPop(); } 957 ``` 958 959 It's generally recommended that it be passed an explicit argument 960 to help ensure that push/push are used in matching pairs, but in 961 trivial code that may be a non-issue. 962 */ 963 target.scopedAllocPop = function(state){ 964 __affirmAlloc(this, 'scopedAllocPop'); 965 const n = arguments.length 966 ? cache.scopedAlloc.indexOf(state) 967 : cache.scopedAlloc.length-1; 968 if(n<0) toss("Invalid state object for scopedAllocPop()."); 969 if(0===arguments.length) state = cache.scopedAlloc[n]; 970 cache.scopedAlloc.splice(n,1); 971 for(let p; (p = state.pop()); ) this.dealloc(p); 972 }.bind(target); 973 974 /** 975 Allocates n bytes of memory using this.alloc() and records that 976 fact in the state for the most recent call of scopedAllocPush(). 977 Ownership of the memory is given to scopedAllocPop(), which 978 will clean it up when it is called. The memory _must not_ be 979 passed to this.dealloc(). Throws if this API object is missing 980 the required `alloc()` or `dealloc()` functions or no scoped 981 alloc is active. 982 983 See scopedAllocPush() for an example of how to use this function. 984 985 The `level` property of this function can be queried to query how 986 many scoped allocation levels are currently active. 987 988 See also: scopedAllocPtr(), scopedAllocCString() 989 */ 990 target.scopedAlloc = function(n){ 991 if(!cache.scopedAlloc.length){ 992 toss("No scopedAllocPush() scope is active."); 993 } 994 const p = this.alloc(n); 995 cache.scopedAlloc[cache.scopedAlloc.length-1].push(p); 996 return p; 997 }.bind(target); 998 999 Object.defineProperty(target.scopedAlloc, 'level', { 1000 configurable: false, enumerable: false, 1001 get: ()=>cache.scopedAlloc.length, 1002 set: ()=>toss("The 'active' property is read-only.") 1003 }); 1004 1005 /** 1006 Works identically to allocCString() except that it allocates the 1007 memory using scopedAlloc(). 1008 1009 Will throw if no scopedAllocPush() call is active. 1010 */ 1011 target.scopedAllocCString = 1012 (jstr, returnWithLength=false)=>__allocCStr(jstr, returnWithLength, 1013 target.scopedAlloc, 'scopedAllocCString()'); 1014 1015 /** 1016 Wraps function call func() in a scopedAllocPush() and 1017 scopedAllocPop() block, such that all calls to scopedAlloc() and 1018 friends from within that call will have their memory freed 1019 automatically when func() returns. If func throws or propagates 1020 an exception, the scope is still popped, otherwise it returns the 1021 result of calling func(). 1022 */ 1023 target.scopedAllocCall = function(func){ 1024 this.scopedAllocPush(); 1025 try{ return func() } finally{ this.scopedAllocPop() } 1026 }.bind(target); 1027 1028 /** Internal impl for allocPtr() and scopedAllocPtr(). */ 1029 const __allocPtr = function(howMany, method){ 1030 __affirmAlloc(this, method); 1031 let m = this[method](howMany * ptrSizeof); 1032 this.setMemValue(m, 0, ptrIR) 1033 if(1===howMany){ 1034 return m; 1035 } 1036 const a = [m]; 1037 for(let i = 1; i < howMany; ++i){ 1038 m += ptrSizeof; 1039 a[i] = m; 1040 this.setMemValue(m, 0, ptrIR); 1041 } 1042 return a; 1043 }.bind(target); 1044 1045 /** 1046 Allocates a single chunk of memory capable of holding `howMany` 1047 pointers and zeroes them out. If `howMany` is 1 then the memory 1048 chunk is returned directly, else an array of pointer addresses is 1049 returned, which can optionally be used with "destructuring 1050 assignment" like this: 1051 1052 ``` 1053 const [p1, p2, p3] = allocPtr(3); 1054 ``` 1055 1056 ACHTUNG: when freeing the memory, pass only the _first_ result 1057 value to dealloc(). The others are part of the same memory chunk 1058 and must not be freed separately. 1059 */ 1060 target.allocPtr = (howMany=1)=>__allocPtr(howMany, 'alloc'); 1061 1062 /** 1063 Identical to allocPtr() except that it allocates using scopedAlloc() 1064 instead of alloc(). 1065 */ 1066 target.scopedAllocPtr = (howMany=1)=>__allocPtr(howMany, 'scopedAlloc'); 1067 1068 /** 1069 If target.exports[name] exists, it is returned, else an 1070 exception is thrown. 1071 */ 1072 target.xGet = function(name){ 1073 return target.exports[name] || toss("Cannot find exported symbol:",name); 1074 }; 1075 1076 const __argcMismatch = 1077 (f,n)=>toss(f+"() requires",n,"argument(s)."); 1078 1079 /** 1080 Looks up a WASM-exported function named fname from 1081 target.exports. If found, it is called, passed all remaining 1082 arguments, and its return value is returned to xCall's caller. If 1083 not found, an exception is thrown. This function does no 1084 conversion of argument or return types, but see xWrap() and 1085 xCallWrapped() for variants which do. 1086 1087 As a special case, if passed only 1 argument after the name and 1088 that argument in an Array, that array's entries become the 1089 function arguments. (This is not an ambiguous case because it's 1090 not legal to pass an Array object to a WASM function.) 1091 */ 1092 target.xCall = function(fname, ...args){ 1093 const f = this.xGet(fname); 1094 if(!(f instanceof Function)) toss("Exported symbol",fname,"is not a function."); 1095 if(f.length!==args.length) __argcMismatch(fname,f.length) 1096 /* This is arguably over-pedantic but we want to help clients keep 1097 from shooting themselves in the foot when calling C APIs. */; 1098 return (2===arguments.length && Array.isArray(arguments[1])) 1099 ? f.apply(null, arguments[1]) 1100 : f.apply(null, args); 1101 }.bind(target); 1102 1103 /** 1104 State for use with xWrap() 1105 */ 1106 cache.xWrap = Object.create(null); 1107 const xcv = cache.xWrap.convert = Object.create(null); 1108 /** Map of type names to argument conversion functions. */ 1109 cache.xWrap.convert.arg = Object.create(null); 1110 /** Map of type names to return result conversion functions. */ 1111 cache.xWrap.convert.result = Object.create(null); 1112 1113 xcv.arg.i64 = (i)=>BigInt(i); 1114 xcv.arg.i32 = (i)=>(i | 0); 1115 xcv.arg.i16 = (i)=>((i | 0) & 0xFFFF); 1116 xcv.arg.i8 = (i)=>((i | 0) & 0xFF); 1117 xcv.arg.f32 = xcv.arg.float = (i)=>Number(i).valueOf(); 1118 xcv.arg.f64 = xcv.arg.double = xcv.arg.f32; 1119 xcv.arg.int = xcv.arg.i32; 1120 xcv.result['*'] = xcv.result['pointer'] = xcv.arg[ptrIR]; 1121 1122 for(const t of ['i8', 'i16', 'i32', 'int', 'i64', 1123 'f32', 'float', 'f64', 'double']){ 1124 xcv.arg[t+'*'] = xcv.result[t+'*'] = xcv.arg[ptrIR] 1125 xcv.result[t] = xcv.arg[t] || toss("Missing arg converter:",t); 1126 } 1127 xcv.arg['**'] = xcv.arg[ptrIR]; 1128 1129 /** 1130 In order for args of type string to work in various contexts in 1131 the sqlite3 API, we need to pass them on as, variably, a C-string 1132 or a pointer value. Thus for ARGs of type 'string' and 1133 '*'/'pointer' we behave differently depending on whether the 1134 argument is a string or not: 1135 1136 - If v is a string, scopeAlloc() a new C-string from it and return 1137 that temp string's pointer. 1138 1139 - Else return the value from the arg adaptor defined for ptrIR. 1140 1141 TODO? Permit an Int8Array/Uint8Array and convert it to a string? 1142 Would that be too much magic concentrated in one place, ready to 1143 backfire? 1144 */ 1145 xcv.arg.string = xcv.arg['pointer'] = xcv.arg['*'] = function(v){ 1146 if('string'===typeof v) return target.scopedAllocCString(v); 1147 return v ? xcv.arg[ptrIR](v) : null; 1148 }; 1149 xcv.result.string = (i)=>target.cstringToJs(i); 1150 xcv.result['string:free'] = function(i){ 1151 try { return i ? target.cstringToJs(i) : null } 1152 finally{ target.dealloc(i) } 1153 }; 1154 xcv.result.json = (i)=>JSON.parse(target.cstringToJs(i)); 1155 xcv.result['json:free'] = function(i){ 1156 try{ return i ? JSON.parse(target.cstringToJs(i)) : null } 1157 finally{ target.dealloc(i) } 1158 } 1159 xcv.result['void'] = (v)=>undefined; 1160 xcv.result['null'] = (v)=>v; 1161 1162 if(0){ 1163 /*** 1164 This idea can't currently work because we don't know the 1165 signature for the func and don't have a way for the user to 1166 convey it. To do this we likely need to be able to match 1167 arg/result handlers by a regex, but that would incur an O(N) 1168 cost as we check the regex one at a time. Another use case for 1169 such a thing would be pseudotypes like "int:-1" to say that 1170 the value will always be treated like -1 (which has a useful 1171 case in the sqlite3 bindings). 1172 */ 1173 xcv.arg['func-ptr'] = function(v){ 1174 if(!(v instanceof Function)) return xcv.arg[ptrIR]; 1175 const f = this.jsFuncToWasm(v, WHAT_SIGNATURE); 1176 }.bind(target); 1177 } 1178 1179 const __xArgAdapter = 1180 (t)=>xcv.arg[t] || toss("Argument adapter not found:",t); 1181 1182 const __xResultAdapter = 1183 (t)=>xcv.result[t] || toss("Result adapter not found:",t); 1184 1185 cache.xWrap.convertArg = (t,v)=>__xArgAdapter(t)(v); 1186 cache.xWrap.convertResult = 1187 (t,v)=>(null===t ? v : (t ? __xResultAdapter(t)(v) : undefined)); 1188 1189 /** 1190 Creates a wrapper for the WASM-exported function fname. Uses 1191 xGet() to fetch the exported function (which throws on 1192 error) and returns either that function or a wrapper for that 1193 function which converts the JS-side argument types into WASM-side 1194 types and converts the result type. If the function takes no 1195 arguments and resultType is `null` then the function is returned 1196 as-is, else a wrapper is created for it to adapt its arguments 1197 and result value, as described below. 1198 1199 (If you're familiar with Emscripten's ccall() and cwrap(), this 1200 function is essentially cwrap() on steroids.) 1201 1202 This function's arguments are: 1203 1204 - fname: the exported function's name. xGet() is used to fetch 1205 this, so will throw if no exported function is found with that 1206 name. 1207 1208 - resultType: the name of the result type. A literal `null` means 1209 to return the original function's value as-is (mnemonic: there 1210 is "null" conversion going on). Literal `undefined` or the 1211 string `"void"` mean to ignore the function's result and return 1212 `undefined`. Aside from those two special cases, it may be one 1213 of the values described below or any mapping installed by the 1214 client using xWrap.resultAdapter(). 1215 1216 If passed 3 arguments and the final one is an array, that array 1217 must contain a list of type names (see below) for adapting the 1218 arguments from JS to WASM. If passed 2 arguments, more than 3, 1219 or the 3rd is not an array, all arguments after the 2nd (if any) 1220 are treated as type names. i.e.: 1221 1222 ``` 1223 xWrap('funcname', 'i32', 'string', 'f64'); 1224 // is equivalent to: 1225 xWrap('funcname', 'i32', ['string', 'f64']); 1226 ``` 1227 1228 Type names are symbolic names which map the arguments to an 1229 adapter function to convert, if needed, the value before passing 1230 it on to WASM or to convert a return result from WASM. The list 1231 of built-in names: 1232 1233 - `i8`, `i16`, `i32` (args and results): all integer conversions 1234 which convert their argument to an integer and truncate it to 1235 the given bit length. 1236 1237 - `N*` (args): a type name in the form `N*`, where N is a numeric 1238 type name, is treated the same as WASM pointer. 1239 1240 - `*` and `pointer` (args): have multple semantics. They 1241 behave exactly as described below for `string` args. 1242 1243 - `*` and `pointer` (results): are aliases for the current 1244 WASM pointer numeric type. 1245 1246 - `**` (args): is simply a descriptive alias for the WASM pointer 1247 type. It's primarily intended to mark output-pointer arguments. 1248 1249 - `i64` (args and results): passes the value to BigInt() to 1250 convert it to an int64. 1251 1252 - `f32` (`float`), `f64` (`double`) (args and results): pass 1253 their argument to Number(). i.e. the adaptor does not currently 1254 distinguish between the two types of floating-point numbers. 1255 1256 Non-numeric conversions include: 1257 1258 - `string` (args): has two different semantics in order to 1259 accommodate various uses of certain C APIs (e.g. output-style 1260 strings)... 1261 1262 - If the arg is a string, it creates a _temporary_ C-string to 1263 pass to the exported function, cleaning it up before the 1264 wrapper returns. If a long-lived C-string pointer is 1265 required, that requires client-side code to create the 1266 string, then pass its pointer to the function. 1267 1268 - Else the arg is assumed to be a pointer to a string the 1269 client has already allocated and it's passed on as 1270 a WASM pointer. 1271 1272 - `string` (results): treats the result value as a const C-string, 1273 copies it to a JS string, and returns that JS string. 1274 1275 - `string:free` (results): treats the result value as a non-const 1276 C-string, ownership of which has just been transfered to the 1277 caller. It copies the C-string to a JS string, frees the 1278 C-string, and returns the JS string. If such a result value is 1279 NULL, the JS result is `null`. 1280 1281 - `json` (results): treats the result as a const C-string and 1282 returns the result of passing the converted-to-JS string to 1283 JSON.parse(). Returns `null` if the C-string is a NULL pointer. 1284 1285 - `json:free` (results): works exactly like `string:free` but 1286 returns the same thing as the `json` adapter. 1287 1288 The type names for results and arguments are validated when 1289 xWrap() is called and any unknown names will trigger an 1290 exception. 1291 1292 Clients may map their own result and argument adapters using 1293 xWrap.resultAdapter() and xWrap.argAdaptor(), noting that not all 1294 type conversions are valid for both arguments _and_ result types 1295 as they often have different memory ownership requirements. 1296 1297 TODOs: 1298 1299 - Figure out how/whether we can (semi-)transparently handle 1300 pointer-type _output_ arguments. Those currently require 1301 explicit handling by allocating pointers, assigning them before 1302 the call using setMemValue(), and fetching them with 1303 getMemValue() after the call. We may be able to automate some 1304 or all of that. 1305 1306 - Figure out whether it makes sense to extend the arg adapter 1307 interface such that each arg adapter gets an array containing 1308 the results of the previous arguments in the current call. That 1309 might allow some interesting type-conversion feature. Use case: 1310 handling of the final argument to sqlite3_prepare_v2() depends 1311 on the type (pointer vs JS string) of its 2nd 1312 argument. Currently that distinction requires hand-writing a 1313 wrapper for that function. That case is unusual enough that 1314 abstracting it into this API (and taking on the associated 1315 costs) may well not make good sense. 1316 */ 1317 target.xWrap = function(fname, resultType, ...argTypes){ 1318 if(3===arguments.length && Array.isArray(arguments[2])){ 1319 argTypes = arguments[2]; 1320 } 1321 const xf = this.xGet(fname); 1322 if(argTypes.length!==xf.length) __argcMismatch(fname, xf.length) 1323 if((null===resultType) && 0===xf.length){ 1324 /* Func taking no args with an as-is return. We don't need a wrapper. */ 1325 return xf; 1326 } 1327 /*Verify the arg type conversions are valid...*/; 1328 if(undefined!==resultType && null!==resultType) __xResultAdapter(resultType); 1329 argTypes.forEach(__xArgAdapter) 1330 if(0===xf.length){ 1331 // No args to convert, so we can create a simpler wrapper... 1332 return function(){ 1333 return (arguments.length 1334 ? __argcMismatch(fname, xf.length) 1335 : cache.xWrap.convertResult(resultType, xf.call(null))); 1336 }; 1337 } 1338 return function(...args){ 1339 if(args.length!==xf.length) __argcMismatch(fname, xf.length); 1340 const scope = this.scopedAllocPush(); 1341 try{ 1342 const rc = xf.apply(null,args.map((v,i)=>cache.xWrap.convertArg(argTypes[i], v))); 1343 return cache.xWrap.convertResult(resultType, rc); 1344 }finally{ 1345 this.scopedAllocPop(scope); 1346 } 1347 }.bind(this); 1348 }.bind(target)/*xWrap()*/; 1349 1350 /** Internal impl for xWrap.resultAdapter() and argAdaptor(). */ 1351 const __xAdapter = function(func, argc, typeName, adapter, modeName, xcvPart){ 1352 if('string'===typeof typeName){ 1353 if(1===argc) return xcvPart[typeName]; 1354 else if(2===argc){ 1355 if(!adapter){ 1356 delete xcvPart[typeName]; 1357 return func; 1358 }else if(!(adapter instanceof Function)){ 1359 toss(modeName,"requires a function argument."); 1360 } 1361 xcvPart[typeName] = adapter; 1362 return func; 1363 } 1364 } 1365 toss("Invalid arguments to",modeName); 1366 }; 1367 1368 /** 1369 Gets, sets, or removes a result value adapter for use with 1370 xWrap(). If passed only 1 argument, the adapter function for the 1371 given type name is returned. If the second argument is explicit 1372 falsy (as opposed to defaulted), the adapter named by the first 1373 argument is removed. If the 2nd argument is not falsy, it must be 1374 a function which takes one value and returns a value appropriate 1375 for the given type name. The adapter may throw if its argument is 1376 not of a type it can work with. This function throws for invalid 1377 arguments. 1378 1379 Example: 1380 1381 ``` 1382 xWrap.resultAdapter('twice',(v)=>v+v); 1383 ``` 1384 1385 xWrap.resultAdapter() MUST NOT use the scopedAlloc() family of 1386 APIs to allocate a result value. xWrap()-generated wrappers run 1387 in the context of scopedAllocPush() so that argument adapters can 1388 easily convert, e.g., to C-strings, and have them cleaned up 1389 automatically before the wrapper returns to the caller. Likewise, 1390 if a _result_ adapter uses scoped allocation, the result will be 1391 freed before because they would be freed before the wrapper 1392 returns, leading to chaos and undefined behavior. 1393 1394 Except when called as a getter, this function returns itself. 1395 */ 1396 target.xWrap.resultAdapter = function f(typeName, adapter){ 1397 return __xAdapter(f, arguments.length, typeName, adapter, 1398 'resultAdaptor()', xcv.result); 1399 }; 1400 1401 /** 1402 Functions identically to xWrap.resultAdapter() but applies to 1403 call argument conversions instead of result value conversions. 1404 1405 xWrap()-generated wrappers perform argument conversion in the 1406 context of a scopedAllocPush(), so any memory allocation 1407 performed by argument adapters really, really, really should be 1408 made using the scopedAlloc() family of functions unless 1409 specifically necessary. For example: 1410 1411 ``` 1412 xWrap.argAdapter('my-string', function(v){ 1413 return ('string'===typeof v) 1414 ? myWasmObj.scopedAllocCString(v) : null; 1415 }; 1416 ``` 1417 1418 Contrariwise, xWrap.resultAdapter() must _not_ use scopedAlloc() 1419 to allocate its results because they would be freed before the 1420 xWrap()-created wrapper returns. 1421 1422 Note that it is perfectly legitimate to use these adapters to 1423 perform argument validation, as opposed (or in addition) to 1424 conversion. 1425 */ 1426 target.xWrap.argAdapter = function f(typeName, adapter){ 1427 return __xAdapter(f, arguments.length, typeName, adapter, 1428 'argAdaptor()', xcv.arg); 1429 }; 1430 1431 /** 1432 Functions like xCall() but performs argument and result type 1433 conversions as for xWrap(). The first argument is the name of the 1434 exported function to call. The 2nd its the name of its result 1435 type, as documented for xWrap(). The 3rd is an array of argument 1436 type name, as documented for xWrap() (use a falsy value or an 1437 empty array for nullary functions). The 4th+ arguments are 1438 arguments for the call, with the special case that if the 4th 1439 argument is an array, it is used as the arguments for the call 1440 (again, falsy or an empty array for nullary functions). Returns 1441 the converted result of the call. 1442 1443 This is just a thin wrapp around xWrap(). If the given function 1444 is to be called more than once, it's more efficient to use 1445 xWrap() to create a wrapper, then to call that wrapper as many 1446 times as needed. For one-shot calls, however, this variant is 1447 arguably more efficient because it will hypothetically free the 1448 wrapper function quickly. 1449 */ 1450 target.xCallWrapped = function(fname, resultType, argTypes, ...args){ 1451 if(Array.isArray(arguments[3])) args = arguments[3]; 1452 return this.xWrap(fname, resultType, argTypes||[]).apply(null, args||[]); 1453 }.bind(target); 1454 1455 return target; 1456}; 1457 1458/** 1459 yawl (Yet Another Wasm Loader) provides very basic wasm loader. 1460 It requires a config object: 1461 1462 - `uri`: required URI of the WASM file to load. 1463 1464 - `onload(loadResult,config)`: optional callback. The first 1465 argument is the result object from 1466 WebAssembly.instanitate[Streaming](). The 2nd is the config 1467 object passed to this function. Described in more detail below. 1468 1469 - `imports`: optional imports object for 1470 WebAssembly.instantiate[Streaming](). The default is am empty set 1471 of imports. If the module requires any imports, this object 1472 must include them. 1473 1474 - `wasmUtilTarget`: optional object suitable for passing to 1475 WhWasmUtilInstaller(). If set, it gets passed to that function 1476 after the promise resolves. This function sets several properties 1477 on it before passing it on to that function (which sets many 1478 more): 1479 1480 - `module`, `instance`: the properties from the 1481 instantiate[Streaming]() result. 1482 1483 - If `instance.exports.memory` is _not_ set then it requires that 1484 `config.imports.env.memory` be set (else it throws), and 1485 assigns that to `target.memory`. 1486 1487 - If `wasmUtilTarget.alloc` is not set and 1488 `instance.exports.malloc` is, it installs 1489 `wasmUtilTarget.alloc()` and `wasmUtilTarget.dealloc()` 1490 wrappers for the exports `malloc` and `free` functions. 1491 1492 It returns a function which, when called, initiates loading of the 1493 module and returns a Promise. When that Promise resolves, it calls 1494 the `config.onload` callback (if set) and passes it 1495 `(loadResult,config)`, where `loadResult` is the result of 1496 WebAssembly.instantiate[Streaming](): an object in the form: 1497 1498 ``` 1499 { 1500 module: a WebAssembly.Module, 1501 instance: a WebAssembly.Instance 1502 } 1503 ``` 1504 1505 (Note that the initial `then()` attached to the promise gets only 1506 that object, and not the `config` one.) 1507 1508 Error handling is up to the caller, who may attach a `catch()` call 1509 to the promise. 1510*/ 1511self.WhWasmUtilInstaller.yawl = function(config){ 1512 const wfetch = ()=>fetch(config.uri, {credentials: 'same-origin'}); 1513 const wui = this; 1514 const finalThen = function(arg){ 1515 //log("finalThen()",arg); 1516 if(config.wasmUtilTarget){ 1517 const toss = (...args)=>{throw new Error(args.join(' '))}; 1518 const tgt = config.wasmUtilTarget; 1519 tgt.module = arg.module; 1520 tgt.instance = arg.instance; 1521 //tgt.exports = tgt.instance.exports; 1522 if(!tgt.instance.exports.memory){ 1523 /** 1524 WhWasmUtilInstaller requires either tgt.exports.memory 1525 (exported from WASM) or tgt.memory (JS-provided memory 1526 imported into WASM). 1527 */ 1528 tgt.memory = (config.imports && config.imports.env 1529 && config.imports.env.memory) 1530 || toss("Missing 'memory' object!"); 1531 } 1532 if(!tgt.alloc && arg.instance.exports.malloc){ 1533 tgt.alloc = function(n){ 1534 return this(n) || toss("Allocation of",n,"bytes failed."); 1535 }.bind(arg.instance.exports.malloc); 1536 tgt.dealloc = function(m){this(m)}.bind(arg.instance.exports.free); 1537 } 1538 wui(tgt); 1539 } 1540 if(config.onload) config.onload(arg,config); 1541 return arg /* for any then() handler attached to 1542 yetAnotherWasmLoader()'s return value */; 1543 }; 1544 const loadWasm = WebAssembly.instantiateStreaming 1545 ? function loadWasmStreaming(){ 1546 return WebAssembly.instantiateStreaming(wfetch(), config.imports||{}) 1547 .then(finalThen); 1548 } 1549 : function loadWasmOldSchool(){ // Safari < v15 1550 return wfetch() 1551 .then(response => response.arrayBuffer()) 1552 .then(bytes => WebAssembly.instantiate(bytes, config.imports||{})) 1553 .then(finalThen); 1554 }; 1555 return loadWasm; 1556}.bind(self.WhWasmUtilInstaller)/*yawl()*/; 1557