tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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());