diff --git a/javascript/CMakeLists.txt b/javascript/CMakeLists.txt index ea3eefd6c1..5b89810435 100644 --- a/javascript/CMakeLists.txt +++ b/javascript/CMakeLists.txt @@ -49,7 +49,7 @@ add_link_options( # both bundlders and Node. "SHELL:-s SINGLE_FILE=1" "SHELL:-s ENVIRONMENT='web'" - "SHELL:-s EXPORTED_RUNTIME_METHODS=['HEAPF32','HEAP32']" + "SHELL:-s EXPORTED_RUNTIME_METHODS=['HEAPF32','HEAP32','stackSave','stackRestore']" "SHELL:-Wl,--export-dynamic") add_library(yogaObjLib OBJECT ${YOGA_SOURCES} ${BRIDGE_SOURCES}) diff --git a/javascript/src/wrapAssembly.ts b/javascript/src/wrapAssembly.ts index f6689a2ca9..596ecaf7e7 100644 --- a/javascript/src/wrapAssembly.ts +++ b/javascript/src/wrapAssembly.ts @@ -939,7 +939,16 @@ export default function wrapAssembly(lib: any): Yoga { height: number | 'auto' | undefined = NaN, direction: Direction = Direction.LTR, ): void { - lib._YGNodeCalculateLayout(this._ptr, width, height, direction); + // An exception thrown by a measure function unwinds the WebAssembly + // frames of the layout without restoring the stack pointer. Restore it, + // or repeated exceptions overflow the stack into static memory. + const stack = lib.stackSave(); + try { + lib._YGNodeCalculateLayout(this._ptr, width, height, direction); + } catch (e) { + lib.stackRestore(stack); + throw e; + } } // --- Layout getters --- diff --git a/javascript/tests/YGMeasureTest.test.ts b/javascript/tests/YGMeasureTest.test.ts index fe651f01ae..27b7533b03 100644 --- a/javascript/tests/YGMeasureTest.test.ts +++ b/javascript/tests/YGMeasureTest.test.ts @@ -64,3 +64,80 @@ test('dont_fail_with_incomplete_measure_dimensions', () => { expect(node3.getComputedWidth()).toBe(100); expect(node3.getComputedHeight()).toBe(0); }); + +function createTreeWithThrowingMeasureFunc(depth: number) { + const root = Yoga.Node.create(); + let parent = root; + for (let i = 0; i < depth; i++) { + const child = Yoga.Node.create(); + parent.insertChild(child, 0); + parent = child; + } + + const leaf = Yoga.Node.create(); + leaf.setMeasureFunc(() => { + throw new Error('measure failed'); + }); + parent.insertChild(leaf, 0); + + return root; +} + +test('measure_func_exception_propagates_to_caller', () => { + const root = createTreeWithThrowingMeasureFunc(0); + + expect(() => + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR), + ).toThrow('measure failed'); +}); + +test('layout_works_after_repeated_measure_func_exceptions', () => { + // Without restoring the stack pointer, this breaks after about 18 throws. + for (let i = 0; i < 1000; i++) { + const root = createTreeWithThrowingMeasureFunc(20); + expect(() => + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR), + ).toThrow('measure failed'); + } + + const root = Yoga.Node.create(); + root.setWidth(100); + root.setHeight(100); + root.setAlignItems(Yoga.ALIGN_FLEX_START); + + const root_child0 = Yoga.Node.create(); + root_child0.setMeasureFunc(() => ({width: 10, height: 10})); + root.insertChild(root_child0, 0); + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR); + + expect(root_child0.getComputedWidth()).toBe(10); + expect(root_child0.getComputedHeight()).toBe(10); +}); + +test('node_is_measured_again_after_its_measure_func_threw', () => { + let shouldThrow = true; + + const root = Yoga.Node.create(); + root.setWidth(100); + root.setHeight(100); + root.setAlignItems(Yoga.ALIGN_FLEX_START); + + const root_child0 = Yoga.Node.create(); + root_child0.setMeasureFunc(() => { + if (shouldThrow) { + throw new Error('measure failed'); + } + return {width: 10, height: 20}; + }); + root.insertChild(root_child0, 0); + + expect(() => + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR), + ).toThrow('measure failed'); + + shouldThrow = false; + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR); + + expect(root_child0.getComputedWidth()).toBe(10); + expect(root_child0.getComputedHeight()).toBe(20); +});