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 */ 4export 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 */ 8export const exposeConstructor = Symbol("exposeConstructor"); 9 10export 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 15export function readString16(wasm, ptr, len) { 16 const buf = new Uint16Array(wasm.memory.buffer, ptr, len); 17 return String.fromCharCode.apply(null, buf) 18} 19 20export 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 */ 50export 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 */ 57export 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*/ 64export 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 */ 72export 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*/ 85export 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*/ 97export 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 if `needsPaddingFields`` is set. 114* 115* See wasm_abi_quirks.md's section on Unions for understanding this ABI. 116*/ 117export function optionToArgsForCalling(jsValue, size, align, needsPaddingFields, 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 args = args.concat(maybePaddingFields(needsPaddingFields, size / align)); 144 return args; 145} 146 147 148/** 149* Given `ptr` in Wasm memory, treat it as an Option<T> with size for type T, 150* and return the converted T (converted using `readCallback(wasm, ptr)`) if the Option is Some 151* else None. 152*/ 153export function readOption(wasm, ptr, size, readCallback) { 154 // Don't need the alignment: diplomat types don't have overridden alignment, 155 // so the flag will immediately be after the inner struct. 156 let flag = resultFlag(wasm, ptr, size); 157 if (flag) { 158 return readCallback(wasm, ptr); 159 } else { 160 return null; 161 } 162} 163 164/** 165 * A wrapper around a slice of WASM memory that can be freed manually or 166 * automatically by the garbage collector. 167 * 168 * This type is necessary for Rust functions that take a `&str` or `&[T]`, since 169 * they can create an edge to this object if they borrow from the str/slice, 170 * or we can manually free the WASM memory if they don't. 171 */ 172export class DiplomatBuf { 173 static str8 = (wasm, string) => { 174 var utf8Length = 0; 175 for (const codepointString of string) { 176 let codepoint = codepointString.codePointAt(0); 177 if (codepoint < 0x80) { 178 utf8Length += 1 179 } else if (codepoint < 0x800) { 180 utf8Length += 2 181 } else if (codepoint < 0x10000) { 182 utf8Length += 3 183 } else { 184 utf8Length += 4 185 } 186 } 187 188 const ptr = wasm.diplomat_alloc(utf8Length, 1); 189 190 const result = (new TextEncoder()).encodeInto(string, new Uint8Array(wasm.memory.buffer, ptr, utf8Length)); 191 console.assert(string.length === result.read && utf8Length === result.written, "UTF-8 write error"); 192 193 return new DiplomatBuf(ptr, utf8Length, () => wasm.diplomat_free(ptr, utf8Length, 1)); 194 } 195 196 static str16 = (wasm, string) => { 197 const byteLength = string.length * 2; 198 const ptr = wasm.diplomat_alloc(byteLength, 2); 199 200 const destination = new Uint16Array(wasm.memory.buffer, ptr, string.length); 201 for (let i = 0; i < string.length; i++) { 202 destination[i] = string.charCodeAt(i); 203 } 204 205 return new DiplomatBuf(ptr, string.length, () => wasm.diplomat_free(ptr, byteLength, 2)); 206 } 207 208 static slice = (wasm, list, rustType) => { 209 const elementSize = rustType === "u8" || rustType === "i8" || rustType === "boolean" ? 1 : 210 rustType === "u16" || rustType === "i16" ? 2 : 211 rustType === "u64" || rustType === "i64" || rustType === "f64" ? 8 : 212 4; 213 214 const byteLength = list.length * elementSize; 215 const ptr = wasm.diplomat_alloc(byteLength, elementSize); 216 217 /** 218 * Create an array view of the buffer. This gives us the `set` method which correctly handles untyped values 219 */ 220 const destination = 221 rustType === "u8" || rustType === "boolean" ? new Uint8Array(wasm.memory.buffer, ptr, byteLength) : 222 rustType === "i8" ? new Int8Array(wasm.memory.buffer, ptr, byteLength) : 223 rustType === "u16" ? new Uint16Array(wasm.memory.buffer, ptr, byteLength) : 224 rustType === "i16" ? new Int16Array(wasm.memory.buffer, ptr, byteLength) : 225 rustType === "i32" ? new Int32Array(wasm.memory.buffer, ptr, byteLength) : 226 rustType === "u64" ? new BigUint64Array(wasm.memory.buffer, ptr, byteLength) : 227 rustType === "i64" ? new BigInt64Array(wasm.memory.buffer, ptr, byteLength) : 228 rustType === "f32" ? new Float32Array(wasm.memory.buffer, ptr, byteLength) : 229 rustType === "f64" ? new Float64Array(wasm.memory.buffer, ptr, byteLength) : 230 new Uint32Array(wasm.memory.buffer, ptr, byteLength); 231 destination.set(list); 232 233 return new DiplomatBuf(ptr, list.length, () => wasm.diplomat_free(ptr, byteLength, elementSize)); 234 } 235 236 237 static strs = (wasm, strings, encoding) => { 238 let encodeStr = (encoding === "string16") ? DiplomatBuf.str16 : DiplomatBuf.str8; 239 240 const byteLength = strings.length * 4 * 2; 241 242 const ptr = wasm.diplomat_alloc(byteLength, 4); 243 244 const destination = new Uint32Array(wasm.memory.buffer, ptr, byteLength); 245 246 const stringsAlloc = []; 247 248 for (let i = 0; i < strings.length; i++) { 249 stringsAlloc.push(encodeStr(wasm, strings[i])); 250 251 destination[2 * i] = stringsAlloc[i].ptr; 252 destination[(2 * i) + 1] = stringsAlloc[i].size; 253 } 254 255 return new DiplomatBuf(ptr, strings.length, () => { 256 wasm.diplomat_free(ptr, byteLength, 4); 257 for (let i = 0; i < stringsAlloc.length; i++) { 258 stringsAlloc[i].free(); 259 } 260 }); 261 } 262 263 /** 264 * Generated code calls one of methods these for each allocation, to either 265 * free directly after the FFI call, to leak (to create a &'static), or to 266 * register the buffer with the garbage collector (to create a &'a). 267 */ 268 free; 269 270 constructor(ptr, size, free) { 271 this.ptr = ptr; 272 this.size = size; 273 this.free = free; 274 this.leak = () => { }; 275 this.releaseToGarbageCollector = () => DiplomatBufferFinalizer.register(this, this.free); 276 } 277 278 splat() { 279 return [this.ptr, this.size]; 280 } 281 282 /** 283 * Write the (ptr, len) pair to an array buffer at byte offset `offset` 284 */ 285 writePtrLenToArrayBuffer(arrayBuffer, offset) { 286 writeToArrayBuffer(arrayBuffer, offset, this.ptr, Uint32Array); 287 writeToArrayBuffer(arrayBuffer, offset + 4, this.size, Uint32Array); 288 } 289} 290 291/** 292 * Helper class for creating and managing `diplomat_buffer_write`. 293 * Meant to minimize direct calls to `wasm`. 294 */ 295export class DiplomatWriteBuf { 296 leak; 297 298 #wasm; 299 #buffer; 300 301 constructor(wasm) { 302 this.#wasm = wasm; 303 this.#buffer = this.#wasm.diplomat_buffer_write_create(0); 304 305 this.leak = () => { }; 306 } 307 308 free() { 309 this.#wasm.diplomat_buffer_write_destroy(this.#buffer); 310 } 311 312 releaseToGarbageCollector() { 313 DiplomatBufferFinalizer.register(this, this.free); 314 } 315 316 readString8() { 317 return readString8(this.#wasm, this.ptr, this.size); 318 } 319 320 get buffer() { 321 return this.#buffer; 322 } 323 324 get ptr() { 325 return this.#wasm.diplomat_buffer_write_get_bytes(this.#buffer); 326 } 327 328 get size() { 329 return this.#wasm.diplomat_buffer_write_len(this.#buffer); 330 } 331} 332 333/** 334 * Represents an underlying slice that we've grabbed from WebAssembly. 335 * You can treat this in JS as a regular slice of primitives, but it handles additional data for you behind the scenes. 336 */ 337export class DiplomatSlice { 338 #wasm; 339 340 #bufferType; 341 get bufferType() { 342 return this.#bufferType; 343 } 344 345 #buffer; 346 get buffer() { 347 return this.#buffer; 348 } 349 350 #lifetimeEdges; 351 352 constructor(wasm, buffer, bufferType, lifetimeEdges) { 353 this.#wasm = wasm; 354 355 const [ptr, size] = new Uint32Array(this.#wasm.memory.buffer, buffer, 2); 356 357 this.#buffer = new bufferType(this.#wasm.memory.buffer, ptr, size); 358 this.#bufferType = bufferType; 359 360 this.#lifetimeEdges = lifetimeEdges; 361 } 362 363 getValue() { 364 return this.#buffer; 365 } 366 367 [Symbol.toPrimitive]() { 368 return this.getValue(); 369 } 370 371 valueOf() { 372 return this.getValue(); 373 } 374} 375 376export class DiplomatSlicePrimitive extends DiplomatSlice { 377 constructor(wasm, buffer, sliceType, lifetimeEdges) { 378 const [ptr, size] = new Uint32Array(wasm.memory.buffer, buffer, 2); 379 380 let arrayType; 381 switch (sliceType) { 382 case "u8": 383 case "boolean": 384 arrayType = Uint8Array; 385 break; 386 case "i8": 387 arrayType = Int8Array; 388 break; 389 case "u16": 390 arrayType = Uint16Array; 391 break; 392 case "i16": 393 arrayType = Int16Array; 394 break; 395 case "i32": 396 arrayType = Int32Array; 397 break; 398 case "u32": 399 arrayType = Uint32Array; 400 break; 401 case "i64": 402 arrayType = BigInt64Array; 403 break; 404 case "u64": 405 arrayType = BigUint64Array; 406 break; 407 case "f32": 408 arrayType = Float32Array; 409 break; 410 case "f64": 411 arrayType = Float64Array; 412 break; 413 default: 414 console.error("Unrecognized bufferType ", bufferType); 415 } 416 417 super(wasm, buffer, arrayType, lifetimeEdges); 418 } 419} 420 421export class DiplomatSliceStr extends DiplomatSlice { 422 #decoder; 423 424 constructor(wasm, buffer, stringEncoding, lifetimeEdges) { 425 let encoding; 426 switch (stringEncoding) { 427 case "string8": 428 encoding = Uint8Array; 429 break; 430 case "string16": 431 encoding = Uint16Array; 432 break; 433 default: 434 console.error("Unrecognized stringEncoding ", stringEncoding); 435 break; 436 } 437 super(wasm, buffer, encoding, lifetimeEdges); 438 439 if (stringEncoding === "string8") { 440 this.#decoder = new TextDecoder('utf-8'); 441 } 442 } 443 444 getValue() { 445 switch (this.bufferType) { 446 case Uint8Array: 447 return this.#decoder.decode(super.getValue()); 448 case Uint16Array: 449 return String.fromCharCode.apply(null, super.getValue()); 450 default: 451 return null; 452 } 453 } 454 455 toString() { 456 return this.getValue(); 457 } 458} 459 460export class DiplomatSliceStrings extends DiplomatSlice { 461 #strings = []; 462 constructor(wasm, buffer, stringEncoding, lifetimeEdges) { 463 super(wasm, buffer, Uint32Array, lifetimeEdges); 464 465 for (let i = this.buffer.byteOffset; i < this.buffer.byteLength; i += this.buffer.BYTES_PER_ELEMENT * 2) { 466 this.#strings.push(new DiplomatSliceStr(wasm, i, stringEncoding, lifetimeEdges)); 467 } 468 } 469 470 getValue() { 471 return this.#strings; 472 } 473} 474 475/** 476 * A number of Rust functions in WebAssembly require a buffer to populate struct, slice, Option<> or Result<> types with information. 477 * {@link DiplomatReceiveBuf} allocates a buffer in WebAssembly, which can then be passed into functions with the {@link DiplomatReceiveBuf.buffer} 478 * property. 479 */ 480export class DiplomatReceiveBuf { 481 #wasm; 482 483 #size; 484 #align; 485 486 #hasResult; 487 488 #buffer; 489 490 constructor(wasm, size, align, hasResult) { 491 this.#wasm = wasm; 492 493 this.#size = size; 494 this.#align = align; 495 496 this.#hasResult = hasResult; 497 498 this.#buffer = this.#wasm.diplomat_alloc(this.#size, this.#align); 499 500 this.leak = () => { }; 501 } 502 503 free() { 504 this.#wasm.diplomat_free(this.#buffer, this.#size, this.#align); 505 } 506 507 get buffer() { 508 return this.#buffer; 509 } 510 511 /** 512 * Only for when a DiplomatReceiveBuf is allocating a buffer for an `Option<>` or a `Result<>` type. 513 * 514 * This just checks the last byte for a successful result (assuming that Rust's compiler does not change). 515 */ 516 get resultFlag() { 517 if (this.#hasResult) { 518 return resultFlag(this.#wasm, this.#buffer, this.#size - 1); 519 } else { 520 return true; 521 } 522 } 523} 524 525/** 526 * For cleaning up slices inside struct _intoFFI functions. 527 * Based somewhat on how the Dart backend handles slice cleanup. 528 * 529 * 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. 530 * 531 * 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. 532 */ 533export class CleanupArena { 534 #items = []; 535 536 constructor() { 537 } 538 539 /** 540 * When this arena is freed, call .free() on the given item. 541 * @param {DiplomatBuf} item 542 * @returns {DiplomatBuf} 543 */ 544 alloc(item) { 545 this.#items.push(item); 546 return item; 547 } 548 /** 549 * Create a new CleanupArena, append it to any edge arrays passed down, and return it. 550 * @param {Array} edgeArrays 551 * @returns {CleanupArena} 552 */ 553 createWith(...edgeArrays) { 554 let self = new CleanupArena(); 555 for (edgeArray of edgeArrays) { 556 if (edgeArray != null) { 557 edgeArray.push(self); 558 } 559 } 560 DiplomatBufferFinalizer.register(self, self.free); 561 return self; 562 } 563 564 /** 565 * If given edge arrays, create a new CleanupArena, append it to any edge arrays passed down, and return it. 566 * Else return the function-local cleanup arena 567 * @param {CleanupArena} functionCleanupArena 568 * @param {Array} edgeArrays 569 * @returns {DiplomatBuf} 570 */ 571 maybeCreateWith(functionCleanupArena, ...edgeArrays) { 572 if (edgeArrays.length > 0) { 573 return CleanupArena.createWith(...edgeArrays); 574 } else { 575 return functionCleanupArena 576 } 577 } 578 579 free() { 580 this.#items.forEach((i) => { 581 i.free(); 582 }); 583 584 this.#items.length = 0; 585 } 586} 587 588/** 589 * Similar to {@link CleanupArena}, but for holding on to slices until a method is called, 590 * after which we rely on the GC to free them. 591 * 592 * This is when you may want to use a slice longer than the body of the method. 593 * 594 * At first glance this seems unnecessary, since we will be holding these slices in edge arrays anyway, 595 * however, if an edge array ends up unused, then we do actually need something to hold it for the duration 596 * of the method call. 597 */ 598export class GarbageCollectorGrip { 599 #items = []; 600 601 alloc(item) { 602 this.#items.push(item); 603 return item; 604 } 605 606 releaseToGarbageCollector() { 607 this.#items.forEach((i) => { 608 i.releaseToGarbageCollector(); 609 }); 610 611 this.#items.length = 0; 612 } 613} 614 615const DiplomatBufferFinalizer = new FinalizationRegistry(free => free()); 616