diff --git a/src/blob.ts b/src/blob.ts index 58e56ed..7ce422c 100644 --- a/src/blob.ts +++ b/src/blob.ts @@ -1,4 +1,4 @@ -import { Module, exports, freeFuncPtr, track } from "./helpers"; +import { Module, exports, freeFuncPtr, malloc, track } from "./helpers"; /** * An object representing a {@link https://harfbuzz.github.io/harfbuzz-hb-blob.html | HarfBuzz blob}. @@ -12,7 +12,7 @@ export class Blob { */ constructor(data: Uint8Array | ArrayBuffer) { const array = data instanceof Uint8Array ? data : new Uint8Array(data); - const blobPtr = exports.malloc(array.byteLength); + const blobPtr = malloc(array.byteLength); Module.HEAPU8.set(array, blobPtr); this.ptr = exports.hb_blob_create( blobPtr, diff --git a/src/buffer.ts b/src/buffer.ts index 442925e..dfd3b53 100644 --- a/src/buffer.ts +++ b/src/buffer.ts @@ -1,6 +1,7 @@ import { Module, exports, + malloc, track, hb_tag, utf8_ptr_to_string, @@ -134,7 +135,7 @@ export class Buffer { itemOffset: number = 0, itemLength?: number, ): void { - const codePointsPtr = exports.malloc(codePoints.length * 4); + const codePointsPtr = malloc(codePoints.length * 4); const codePointsArray = Module.HEAPU32.subarray( codePointsPtr / 4, codePointsPtr / 4 + codePoints.length, @@ -411,7 +412,7 @@ export class Buffer { const sp = Module.stackSave(); const endPos = end ?? this.getLength(); const bufLen = 32 * 1024; - const bufPtr = exports.malloc(bufLen); + const bufPtr = malloc(bufLen); const bufConsumedPtr = Module.stackAlloc(4); let result = ""; while (start < endPos) { diff --git a/src/face.ts b/src/face.ts index 203aa35..eed15cf 100644 --- a/src/face.ts +++ b/src/face.ts @@ -1,6 +1,7 @@ import { Module, exports, + malloc, track, STATIC_ARRAY_SIZE, hb_tag, @@ -343,8 +344,8 @@ export class Face { const languagePtr = language_from_string(language); const nameLen = exports.hb_ot_name_get_utf16(this.ptr, nameId, languagePtr, 0, 0) + 1; + const textPtr = malloc(nameLen * 2); const textSizePtr = Module.stackAlloc(4); - const textPtr = exports.malloc(nameLen * 2); Module.HEAPU32[textSizePtr / 4] = nameLen; exports.hb_ot_name_get_utf16( this.ptr, diff --git a/src/helpers.ts b/src/helpers.ts index 02db097..742f5d3 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -30,6 +30,17 @@ export function track( registry.register(obj, () => destroy(ptr)); } +/** + * Allocate `size` bytes in the WASM memory. Unlike `exports.malloc()`, throws + * if the allocation fails instead of returning 0, which the callers would + * write through, overwriting the module's own data at the start of the memory. + */ +export function malloc(size: number): number { + const ptr = exports.malloc(size); + if (!ptr && size) throw new Error(`Failed to allocate ${size} bytes`); + return ptr; +} + /** * Initialize the HarfBuzz module. * @param module The Emscripten module instance created with {@link @@ -78,7 +89,7 @@ export function utf16_ptr_to_string(ptr: number, length: number): string { * Faster than encoding to UTF-8 */ export function string_to_ascii_ptr(text: string): StringPtr { - const ptr = exports.malloc(text.length + 1); + const ptr = malloc(text.length + 1); for (let i = 0; i < text.length; ++i) { const char = text.charCodeAt(i); if (char > 127) throw new Error("Expected ASCII text"); @@ -95,7 +106,7 @@ export function string_to_ascii_ptr(text: string): StringPtr { } export function string_to_utf8_ptr(text: string): StringPtr { - const ptr = exports.malloc(text.length); + const ptr = malloc(text.length); utf8Encoder.encodeInto(text, Module.HEAPU8.subarray(ptr, ptr + text.length)); return { ptr, @@ -107,7 +118,7 @@ export function string_to_utf8_ptr(text: string): StringPtr { } export function string_to_utf16_ptr(text: string): StringPtr { - const ptr = exports.malloc(text.length * 2); + const ptr = malloc(text.length * 2); const words = Module.HEAPU16.subarray(ptr / 2, ptr / 2 + text.length); for (let i = 0; i < words.length; ++i) words[i] = text.charCodeAt(i); return { @@ -138,7 +149,7 @@ export function language_from_string(str: string): number { */ export function typed_array_from_set(setPtr: number): Uint32Array { const setCount = exports.hb_set_get_population(setPtr); - const arrayPtr = exports.malloc(setCount << 2); + const arrayPtr = malloc(setCount << 2); const arrayOffset = arrayPtr >> 2; exports.hb_set_next_many( setPtr, @@ -146,7 +157,10 @@ export function typed_array_from_set(setPtr: number): Uint32Array { arrayPtr, setCount, ); - const array = Module.HEAPU32.subarray(arrayOffset, arrayOffset + setCount); + // Copy out of the WASM memory: a view of it would be detached when the + // memory grows, and would keep the allocation from being freed. + const array = Module.HEAPU32.slice(arrayOffset, arrayOffset + setCount); + exports.free(arrayPtr); return array; } diff --git a/test/index.test.js b/test/index.test.js index 6ed2abe..9af1d30 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -17,6 +17,19 @@ describe("Face", function () { expect(codepoints).not.to.include("ا".codePointAt(0)); }); + it("collectUnicodes result survives WASM memory growth", function () { + let blob = new hb.Blob( + fs.readFileSync(path.join(__dirname, "fonts/noto/NotoSans-Regular.ttf")), + ); + let face = new hb.Face(blob); + const codepoints = face.collectUnicodes(); + const expected = [...codepoints]; + expect(expected).to.include("a".codePointAt(0)); + // Large enough to grow the WASM memory, which detaches views of the old one. + new hb.Blob(new Uint8Array(64 * 1024 * 1024)); + expect([...codepoints]).to.deep.equal(expected); + }); + it("exposes upem", function () { let blob = new hb.Blob( fs.readFileSync(path.join(__dirname, "fonts/noto/NotoSans-Regular.ttf")), diff --git a/test/malloc.test.js b/test/malloc.test.js new file mode 100644 index 0000000..e550f84 --- /dev/null +++ b/test/malloc.test.js @@ -0,0 +1,15 @@ +import { expect, it } from "vitest"; +import * as hb from "../dist/index.mjs"; + +// Kept in its own file, so that filling up the WASM memory does not affect +// the other tests. +it("Blob throws when malloc fails instead of writing to address 0", function () { + const data = new Uint8Array(64 * 1024 * 1024); + const blobs = []; + expect(function () { + // 64 × 64 MiB is more than the 2 GiB the WASM memory can grow to. + for (let i = 0; i < 64; i++) blobs.push(new hb.Blob(data)); + }).to.throw("Failed to allocate"); + // The module's static data, which lives at the start of the memory, is intact. + expect(hb.versionString()).to.match(/^\d+\.\d+\.\d+$/); +});