diplomat-runtime.mjs (20988B)
1 /** For internal Diplomat use when constructing opaques or out structs. 2 * This is for when we're handling items that we don't want the user to touch, like an structure that's only meant to be output, or de-referencing a pointer we're handed from WASM. 3 */ 4 export const internalConstructor = Symbol("constructor"); 5 /** For internal Diplomat use when accessing a from-fields/from-value constructor that's been overridden by a default constructor. 6 * If we want to pass in arguments without also passing in internalConstructor to avoid triggering some logic we don't want, we use exposeConstructor. 7 */ 8 export const exposeConstructor = Symbol("exposeConstructor"); 9 10 export function readString8(wasm, ptr, len) { 11 const buf = new Uint8Array(wasm.memory.buffer, ptr, len); 12 return (new TextDecoder("utf-8")).decode(buf) 13 } 14 15 export function readString16(wasm, ptr, len) { 16 const buf = new Uint16Array(wasm.memory.buffer, ptr, len); 17 return String.fromCharCode.apply(null, buf) 18 } 19 20 export function withDiplomatWrite(wasm, callback) { 21 const write = wasm.diplomat_buffer_write_create(0); 22 try { 23 callback(write); 24 const outStringPtr = wasm.diplomat_buffer_write_get_bytes(write); 25 if (outStringPtr === null) { 26 throw Error("Out of memory"); 27 } 28 const outStringLen = wasm.diplomat_buffer_write_len(write); 29 return readString8(wasm, outStringPtr, outStringLen); 30 } finally { 31 wasm.diplomat_buffer_write_destroy(write); 32 } 33 } 34 35 /** 36 * Get the pointer returned by an FFI function. 37 * 38 * It's tempting to call `(new Uint32Array(wasm.memory.buffer, FFI_func(), 1))[0]`. 39 * However, there's a chance that `wasm.memory.buffer` will be resized between 40 * the time it's accessed and the time it's used, invalidating the view. 41 * This function ensures that the view into wasm memory is fresh. 42 * 43 * This is used for methods that return multiple types into a wasm buffer, where 44 * one of those types is another ptr. Call this method to get access to the returned 45 * ptr, so the return buffer can be freed. 46 * @param {WebAssembly.Exports} wasm Provided by diplomat generated files. 47 * @param {number} ptr Pointer of a pointer, to be read. 48 * @returns {number} The underlying pointer. 49 */ 50 export function ptrRead(wasm, ptr) { 51 return (new Uint32Array(wasm.memory.buffer, ptr, 1))[0]; 52 } 53 54 /** 55 * Get the flag of a result type. 56 */ 57 export function resultFlag(wasm, ptr, offset) { 58 return (new Uint8Array(wasm.memory.buffer, ptr + offset, 1))[0]; 59 } 60 61 /** 62 * Get the discriminant of a Rust enum. 63 */ 64 export function enumDiscriminant(wasm, ptr) { 65 return (new Int32Array(wasm.memory.buffer, ptr, 1))[0] 66 } 67 68 /** 69 * Return an array of paddingCount zeroes to be spread into a function call 70 * if needsPaddingFields is true, else empty 71 */ 72 export function maybePaddingFields(needsPaddingFields, paddingCount) { 73 if (needsPaddingFields) { 74 return Array(paddingCount).fill(0); 75 } else { 76 return []; 77 } 78 } 79 80 /** 81 * Write a value of width `width` to a an ArrayBuffer `arrayBuffer` 82 * at byte offset `offset`, treating it as a buffer of kind `typedArrayKind` 83 * (which is a `TypedArray` variant like `Uint8Array` or `Int16Array`) 84 */ 85 export function writeToArrayBuffer(arrayBuffer, offset, value, typedArrayKind) { 86 let buffer = new typedArrayKind(arrayBuffer, offset); 87 buffer[0] = value; 88 } 89 90 /** 91 * Take `jsValue` and write it to arrayBuffer at offset `offset` if it is non-null 92 * calling `writeToArrayBufferCallback(arrayBuffer, offset, jsValue)` to write to the buffer, 93 * also writing a tag bit. 94 * 95 * `size` and `align` are the size and alignment of T, not of Option<T> 96 */ 97 export function writeOptionToArrayBuffer(arrayBuffer, offset, jsValue, size, align, writeToArrayBufferCallback) { 98 // perform a nullish check, not a null check, 99 // we want identical behavior for undefined 100 if (jsValue != null) { 101 writeToArrayBufferCallback(arrayBuffer, offset, jsValue); 102 writeToArrayBuffer(arrayBuffer, offset + size, 1, Uint8Array); 103 } 104 } 105 106 /** 107 * For Option<T> of given size/align (of T, not the overall option type), 108 * return an array of fields suitable for passing down to a parameter list. 109 * 110 * Calls writeToArrayBufferCallback(arrayBuffer, offset, jsValue) for non-null jsValues 111 * 112 * This array will have size<T>/align<T> elements for the actual T, then one element 113 * for the is_ok bool, and then align<T> - 1 elements for padding. 114 * 115 * See wasm_abi_quirks.md's section on Unions for understanding this ABI. 116 */ 117 export function optionToArgsForCalling(jsValue, size, align, writeToArrayBufferCallback) { 118 let args; 119 // perform a nullish check, not a null check, 120 // we want identical behavior for undefined 121 if (jsValue != null) { 122 let buffer; 123 // We need our originator array to be properly aligned 124 if (align == 8) { 125 buffer = new BigUint64Array(size / align); 126 } else if (align == 4) { 127 buffer = new Uint32Array(size / align); 128 } else if (align == 2) { 129 buffer = new Uint16Array(size / align); 130 } else { 131 buffer = new Uint8Array(size / align); 132 } 133 134 135 writeToArrayBufferCallback(buffer.buffer, 0, jsValue); 136 args = Array.from(buffer); 137 args.push(1); 138 } else { 139 args = Array(size / align).fill(0); 140 args.push(0); 141 } 142 143 // Unconditionally add padding 144 args = args.concat(Array(align - 1).fill(0)); 145 return args; 146 } 147 148 export function optionToBufferForCalling(wasm, jsValue, size, align, allocator, writeToArrayBufferCallback) { 149 let buf = DiplomatBuf.struct(wasm, size, align); 150 151 let buffer; 152 // Add 1 to the size since we're also accounting for the 0 or 1 is_ok field: 153 if (align == 8) { 154 buffer = new BigUint64Array(wasm.memory.buffer, buf, size / align + 1); 155 } else if (align == 4) { 156 buffer = new Uint32Array(wasm.memory.buffer, buf, size / align + 1); 157 } else if (align == 2) { 158 buffer = new Uint16Array(wasm.memory.buffer, buf, size / align + 1); 159 } else { 160 buffer = new Uint8Array(wasm.memory.buffer, buf, size / align + 1); 161 } 162 163 buffer.fill(0); 164 165 if (jsValue != null) { 166 writeToArrayBufferCallback(buffer.buffer, 0, jsValue); 167 buffer[buffer.length - 1] = 1; 168 } 169 170 allocator.alloc(buf); 171 } 172 173 174 /** 175 * Given `ptr` in Wasm memory, treat it as an Option<T> with size for type T, 176 * and return the converted T (converted using `readCallback(wasm, ptr)`) if the Option is Some 177 * else None. 178 */ 179 export function readOption(wasm, ptr, size, readCallback) { 180 // Don't need the alignment: diplomat types don't have overridden alignment, 181 // so the flag will immediately be after the inner struct. 182 let flag = resultFlag(wasm, ptr, size); 183 if (flag) { 184 return readCallback(wasm, ptr); 185 } else { 186 return null; 187 } 188 } 189 190 /** 191 * A wrapper around a slice of WASM memory that can be freed manually or 192 * automatically by the garbage collector. 193 * 194 * This type is necessary for Rust functions that take a `&str` or `&[T]`, since 195 * they can create an edge to this object if they borrow from the str/slice, 196 * or we can manually free the WASM memory if they don't. 197 */ 198 export class DiplomatBuf { 199 static str8 = (wasm, string) => { 200 var utf8Length = 0; 201 for (const codepointString of string) { 202 let codepoint = codepointString.codePointAt(0); 203 if (codepoint < 0x80) { 204 utf8Length += 1 205 } else if (codepoint < 0x800) { 206 utf8Length += 2 207 } else if (codepoint < 0x10000) { 208 utf8Length += 3 209 } else { 210 utf8Length += 4 211 } 212 } 213 214 const ptr = wasm.diplomat_alloc(utf8Length, 1); 215 216 const result = (new TextEncoder()).encodeInto(string, new Uint8Array(wasm.memory.buffer, ptr, utf8Length)); 217 console.assert(string.length === result.read && utf8Length === result.written, "UTF-8 write error"); 218 219 return new DiplomatBuf(ptr, utf8Length, () => wasm.diplomat_free(ptr, utf8Length, 1)); 220 } 221 222 static str16 = (wasm, string) => { 223 const byteLength = string.length * 2; 224 const ptr = wasm.diplomat_alloc(byteLength, 2); 225 226 const destination = new Uint16Array(wasm.memory.buffer, ptr, string.length); 227 for (let i = 0; i < string.length; i++) { 228 destination[i] = string.charCodeAt(i); 229 } 230 231 return new DiplomatBuf(ptr, string.length, () => wasm.diplomat_free(ptr, byteLength, 2)); 232 } 233 234 static sliceWrapper = (wasm, buf) => { 235 const ptr = wasm.diplomat_alloc(8, 4); 236 let dst = new Uint32Array(wasm.memory.buffer, ptr, 2); 237 238 dst[0] = buf.ptr; 239 dst[1] = buf.size; 240 return new DiplomatBuf(ptr, 8, () => { 241 wasm.diplomat_free(ptr, 8, 4); 242 buf.free(); 243 }); 244 } 245 246 static slice = (wasm, list, rustType) => { 247 const elementSize = rustType === "u8" || rustType === "i8" || rustType === "boolean" ? 1 : 248 rustType === "u16" || rustType === "i16" ? 2 : 249 rustType === "u64" || rustType === "i64" || rustType === "f64" ? 8 : 250 4; 251 252 const byteLength = list.length * elementSize; 253 const ptr = wasm.diplomat_alloc(byteLength, elementSize); 254 255 /** 256 * Create an array view of the buffer. This gives us the `set` method which correctly handles untyped values 257 */ 258 const destination = 259 rustType === "u8" || rustType === "boolean" ? new Uint8Array(wasm.memory.buffer, ptr, byteLength) : 260 rustType === "i8" ? new Int8Array(wasm.memory.buffer, ptr, byteLength) : 261 rustType === "u16" ? new Uint16Array(wasm.memory.buffer, ptr, byteLength) : 262 rustType === "i16" ? new Int16Array(wasm.memory.buffer, ptr, byteLength) : 263 rustType === "i32" ? new Int32Array(wasm.memory.buffer, ptr, byteLength) : 264 rustType === "u64" ? new BigUint64Array(wasm.memory.buffer, ptr, byteLength) : 265 rustType === "i64" ? new BigInt64Array(wasm.memory.buffer, ptr, byteLength) : 266 rustType === "f32" ? new Float32Array(wasm.memory.buffer, ptr, byteLength) : 267 rustType === "f64" ? new Float64Array(wasm.memory.buffer, ptr, byteLength) : 268 new Uint32Array(wasm.memory.buffer, ptr, byteLength); 269 destination.set(list); 270 271 return new DiplomatBuf(ptr, list.length, () => wasm.diplomat_free(ptr, byteLength, elementSize)); 272 } 273 274 static strs = (wasm, strings, encoding) => { 275 let encodeStr = (encoding === "string16") ? DiplomatBuf.str16 : DiplomatBuf.str8; 276 277 const byteLength = strings.length * 4 * 2; 278 279 const ptr = wasm.diplomat_alloc(byteLength, 4); 280 281 const destination = new Uint32Array(wasm.memory.buffer, ptr, byteLength); 282 283 const stringsAlloc = []; 284 285 for (let i = 0; i < strings.length; i++) { 286 stringsAlloc.push(encodeStr(wasm, strings[i])); 287 288 destination[2 * i] = stringsAlloc[i].ptr; 289 destination[(2 * i) + 1] = stringsAlloc[i].size; 290 } 291 292 return new DiplomatBuf(ptr, strings.length, () => { 293 wasm.diplomat_free(ptr, byteLength, 4); 294 for (let i = 0; i < stringsAlloc.length; i++) { 295 stringsAlloc[i].free(); 296 } 297 }); 298 } 299 300 static struct = (wasm, size, align) => { 301 const ptr = wasm.diplomat_alloc(size, align); 302 303 return new DiplomatBuf(ptr, size, () => { 304 wasm.diplomat_free(ptr, size, align); 305 }); 306 } 307 308 /** 309 * Generated code calls one of methods these for each allocation, to either 310 * free directly after the FFI call, to leak (to create a &'static), or to 311 * register the buffer with the garbage collector (to create a &'a). 312 */ 313 free; 314 315 constructor(ptr, size, free) { 316 this.ptr = ptr; 317 this.size = size; 318 this.free = free; 319 this.leak = () => { }; 320 this.releaseToGarbageCollector = () => DiplomatBufferFinalizer.register(this, this.free); 321 } 322 323 splat() { 324 return [this.ptr, this.size]; 325 } 326 327 /** 328 * Write the (ptr, len) pair to an array buffer at byte offset `offset` 329 */ 330 writePtrLenToArrayBuffer(arrayBuffer, offset) { 331 writeToArrayBuffer(arrayBuffer, offset, this.ptr, Uint32Array); 332 writeToArrayBuffer(arrayBuffer, offset + 4, this.size, Uint32Array); 333 } 334 } 335 336 /** 337 * Helper class for creating and managing `diplomat_buffer_write`. 338 * Meant to minimize direct calls to `wasm`. 339 */ 340 export class DiplomatWriteBuf { 341 leak; 342 343 #wasm; 344 #buffer; 345 346 constructor(wasm) { 347 this.#wasm = wasm; 348 this.#buffer = this.#wasm.diplomat_buffer_write_create(0); 349 350 this.leak = () => { }; 351 } 352 353 free() { 354 this.#wasm.diplomat_buffer_write_destroy(this.#buffer); 355 } 356 357 releaseToGarbageCollector() { 358 DiplomatBufferFinalizer.register(this, this.free); 359 } 360 361 readString8() { 362 return readString8(this.#wasm, this.ptr, this.size); 363 } 364 365 get buffer() { 366 return this.#buffer; 367 } 368 369 get ptr() { 370 return this.#wasm.diplomat_buffer_write_get_bytes(this.#buffer); 371 } 372 373 get size() { 374 return this.#wasm.diplomat_buffer_write_len(this.#buffer); 375 } 376 } 377 378 /** 379 * Represents an underlying slice that we've grabbed from WebAssembly. 380 * You can treat this in JS as a regular slice of primitives, but it handles additional data for you behind the scenes. 381 */ 382 export class DiplomatSlice { 383 #wasm; 384 385 #bufferType; 386 get bufferType() { 387 return this.#bufferType; 388 } 389 390 #buffer; 391 get buffer() { 392 return this.#buffer; 393 } 394 395 #lifetimeEdges; 396 397 constructor(wasm, buffer, bufferType, lifetimeEdges) { 398 this.#wasm = wasm; 399 400 const [ptr, size] = new Uint32Array(this.#wasm.memory.buffer, buffer, 2); 401 402 this.#buffer = new bufferType(this.#wasm.memory.buffer, ptr, size); 403 this.#bufferType = bufferType; 404 405 this.#lifetimeEdges = lifetimeEdges; 406 } 407 408 getValue() { 409 return this.#buffer; 410 } 411 412 [Symbol.toPrimitive]() { 413 return this.getValue(); 414 } 415 416 valueOf() { 417 return this.getValue(); 418 } 419 } 420 421 export class DiplomatSlicePrimitive extends DiplomatSlice { 422 constructor(wasm, buffer, sliceType, lifetimeEdges) { 423 const [ptr, size] = new Uint32Array(wasm.memory.buffer, buffer, 2); 424 425 let arrayType; 426 switch (sliceType) { 427 case "u8": 428 case "boolean": 429 arrayType = Uint8Array; 430 break; 431 case "i8": 432 arrayType = Int8Array; 433 break; 434 case "u16": 435 arrayType = Uint16Array; 436 break; 437 case "i16": 438 arrayType = Int16Array; 439 break; 440 case "i32": 441 arrayType = Int32Array; 442 break; 443 case "u32": 444 arrayType = Uint32Array; 445 break; 446 case "i64": 447 arrayType = BigInt64Array; 448 break; 449 case "u64": 450 arrayType = BigUint64Array; 451 break; 452 case "f32": 453 arrayType = Float32Array; 454 break; 455 case "f64": 456 arrayType = Float64Array; 457 break; 458 default: 459 console.error("Unrecognized bufferType ", bufferType); 460 } 461 462 super(wasm, buffer, arrayType, lifetimeEdges); 463 } 464 } 465 466 export class DiplomatSliceStr extends DiplomatSlice { 467 #decoder; 468 469 constructor(wasm, buffer, stringEncoding, lifetimeEdges) { 470 let encoding; 471 switch (stringEncoding) { 472 case "string8": 473 encoding = Uint8Array; 474 break; 475 case "string16": 476 encoding = Uint16Array; 477 break; 478 default: 479 console.error("Unrecognized stringEncoding ", stringEncoding); 480 break; 481 } 482 super(wasm, buffer, encoding, lifetimeEdges); 483 484 if (stringEncoding === "string8") { 485 this.#decoder = new TextDecoder('utf-8'); 486 } 487 } 488 489 getValue() { 490 switch (this.bufferType) { 491 case Uint8Array: 492 return this.#decoder.decode(super.getValue()); 493 case Uint16Array: 494 return String.fromCharCode.apply(null, super.getValue()); 495 default: 496 return null; 497 } 498 } 499 500 toString() { 501 return this.getValue(); 502 } 503 } 504 505 export class DiplomatSliceStrings extends DiplomatSlice { 506 #strings = []; 507 constructor(wasm, buffer, stringEncoding, lifetimeEdges) { 508 super(wasm, buffer, Uint32Array, lifetimeEdges); 509 510 for (let i = this.buffer.byteOffset; i < this.buffer.byteLength; i += this.buffer.BYTES_PER_ELEMENT * 2) { 511 this.#strings.push(new DiplomatSliceStr(wasm, i, stringEncoding, lifetimeEdges)); 512 } 513 } 514 515 getValue() { 516 return this.#strings; 517 } 518 } 519 520 /** 521 * A number of Rust functions in WebAssembly require a buffer to populate struct, slice, Option<> or Result<> types with information. 522 * {@link DiplomatReceiveBuf} allocates a buffer in WebAssembly, which can then be passed into functions with the {@link DiplomatReceiveBuf.buffer} 523 * property. 524 */ 525 export class DiplomatReceiveBuf { 526 #wasm; 527 528 #size; 529 #align; 530 531 #hasResult; 532 533 #buffer; 534 535 constructor(wasm, size, align, hasResult) { 536 this.#wasm = wasm; 537 538 this.#size = size; 539 this.#align = align; 540 541 this.#hasResult = hasResult; 542 543 this.#buffer = this.#wasm.diplomat_alloc(this.#size, this.#align); 544 545 this.leak = () => { }; 546 } 547 548 free() { 549 this.#wasm.diplomat_free(this.#buffer, this.#size, this.#align); 550 } 551 552 get buffer() { 553 return this.#buffer; 554 } 555 556 /** 557 * Only for when a DiplomatReceiveBuf is allocating a buffer for an `Option<>` or a `Result<>` type. 558 * 559 * This just checks the last byte for a successful result (assuming that Rust's compiler does not change). 560 */ 561 get resultFlag() { 562 if (this.#hasResult) { 563 return resultFlag(this.#wasm, this.#buffer, this.#size - 1); 564 } else { 565 return true; 566 } 567 } 568 } 569 570 /** 571 * For cleaning up slices inside struct _intoFFI functions. 572 * Based somewhat on how the Dart backend handles slice cleanup. 573 * 574 * We want to ensure a slice only lasts as long as its struct, so we have a `functionCleanupArena` CleanupArena that we use in each method for any slice that needs to be cleaned up. It lasts only as long as the function is called for. 575 * 576 * Then we have `createWith`, which is meant for longer lasting slices. It takes an array of edges and will last as long as those edges do. Cleanup is only called later. 577 */ 578 export class CleanupArena { 579 #items = []; 580 581 constructor() { 582 } 583 584 /** 585 * When this arena is freed, call .free() on the given item. 586 * @param {DiplomatBuf} item 587 * @returns {DiplomatBuf} 588 */ 589 alloc(item) { 590 this.#items.push(item); 591 return item; 592 } 593 /** 594 * Create a new CleanupArena, append it to any edge arrays passed down, and return it. 595 * @param {Array} edgeArrays 596 * @returns {CleanupArena} 597 */ 598 static createWith(...edgeArrays) { 599 let self = new CleanupArena(); 600 for (let edgeArray of edgeArrays) { 601 if (edgeArray != null) { 602 edgeArray.push(self); 603 } 604 } 605 DiplomatBufferFinalizer.register(self, self.free); 606 return self; 607 } 608 609 /** 610 * If given edge arrays, create a new CleanupArena, append it to any edge arrays passed down, and return it. 611 * Else return the function-local cleanup arena 612 * @param {CleanupArena} functionCleanupArena 613 * @param {Array} edgeArrays 614 * @returns {DiplomatBuf} 615 */ 616 static maybeCreateWith(functionCleanupArena, ...edgeArrays) { 617 if (edgeArrays.length > 0) { 618 return CleanupArena.createWith(...edgeArrays); 619 } else { 620 return functionCleanupArena 621 } 622 } 623 624 free() { 625 this.#items.forEach((i) => { 626 i.free(); 627 }); 628 629 this.#items.length = 0; 630 } 631 } 632 633 /** 634 * Similar to {@link CleanupArena}, but for holding on to slices until a method is called, 635 * after which we rely on the GC to free them. 636 * 637 * This is when you may want to use a slice longer than the body of the method. 638 * 639 * At first glance this seems unnecessary, since we will be holding these slices in edge arrays anyway, 640 * however, if an edge array ends up unused, then we do actually need something to hold it for the duration 641 * of the method call. 642 */ 643 export class GarbageCollectorGrip { 644 #items = []; 645 646 alloc(item) { 647 this.#items.push(item); 648 return item; 649 } 650 651 releaseToGarbageCollector() { 652 this.#items.forEach((i) => { 653 i.releaseToGarbageCollector(); 654 }); 655 656 this.#items.length = 0; 657 } 658 } 659 660 const DiplomatBufferFinalizer = new FinalizationRegistry(free => free());