• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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