From 4978e58414d3dd6b71ec27e0cc89e066ebe1693b Mon Sep 17 00:00:00 2001 From: Thibault Saunier Date: Tue, 17 Mar 2026 22:28:41 -0300 Subject: [PATCH] link: run fpcast-emu before optimization passes Move the `--fpcast-emu` binaryen pass earlier in the pass pipeline so it runs before -O2. Previously, directize (part of -O2) would see type-mismatched call_indirect entries and replace them with unreachable before fpcast-emu had a chance to rewrite the table entries with matching types. This caused crashes for common C idioms like calling a 1-arg function through a 2-arg function pointer (e.g. GLib's `g_list_free_full` pattern). This reordering also improves performance: on a real-world GStreamer test binary, the reordered pipeline produces a 1.8% smaller wasm output (5.30MB vs 5.40MB) and seems to run almost ~2x faster (not sure why). Also move the fpcast-emu block outside `if optimizing:` so it runs even without -O2 in the link flags as this is not an optimization pass in practice. Also add a regression test. See https://github.com/WebAssembly/binaryen/pull/8475 --- ...emulate_function_pointer_casts_directize.c | 22 +++++++++++++++++++ ...ulate_function_pointer_casts_directize.out | 1 + test/test_core.py | 6 +++++ tools/link.py | 10 ++++----- 4 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 test/core/test_emulate_function_pointer_casts_directize.c create mode 100644 test/core/test_emulate_function_pointer_casts_directize.out diff --git a/test/core/test_emulate_function_pointer_casts_directize.c b/test/core/test_emulate_function_pointer_casts_directize.c new file mode 100644 index 0000000000000..702b9c3521193 --- /dev/null +++ b/test/core/test_emulate_function_pointer_casts_directize.c @@ -0,0 +1,22 @@ +/* + * Regression test: fpcast-emu must run before directize (inside -O2). + * Otherwise directize sees the type-mismatched call_indirect (calling a + * 1-arg function through a 2-arg pointer) and replaces it with unreachable. + */ +#include + +typedef void (*two_arg_fn)(void *, void *); + +static void one_arg(void *p) { printf("OK\n"); } + +static two_arg_fn hide_cast(void (*fn)(void *)) { + two_arg_fn result = (two_arg_fn)fn; + __asm__("" : "+r"(result)); + return result; +} + +int main(void) { + two_arg_fn fn = hide_cast(one_arg); + fn((void *)1, (void *)2); + return 0; +} diff --git a/test/core/test_emulate_function_pointer_casts_directize.out b/test/core/test_emulate_function_pointer_casts_directize.out new file mode 100644 index 0000000000000..d86bac9de59ab --- /dev/null +++ b/test/core/test_emulate_function_pointer_casts_directize.out @@ -0,0 +1 @@ +OK diff --git a/test/test_core.py b/test/test_core.py index a6b982413e2f9..f129b2223a7f5 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -7307,6 +7307,12 @@ def test_emulate_function_pointer_casts(self): self.set_setting('EMULATE_FUNCTION_POINTER_CASTS') self.do_core_test('test_emulate_function_pointer_casts.cpp', cflags=['-Wno-deprecated']) + def test_emulate_function_pointer_casts_directize(self): + # Regression test: directize (inside -O2) must not replace type-mismatched + # call_indirect with unreachable when fpcast-emu will fix them. + # https://github.com/WebAssembly/binaryen/pull/8475 + self.do_core_test('test_emulate_function_pointer_casts_directize.c', cflags=['-sEMULATE_FUNCTION_POINTER_CASTS']) + @no_wasm2js('TODO: nicely printed names in wasm2js') @no_bun('https://github.com/emscripten-core/emscripten/issues/26197') @parameterized({ diff --git a/tools/link.py b/tools/link.py index aae0056029897..bc4945830d12f 100644 --- a/tools/link.py +++ b/tools/link.py @@ -344,6 +344,11 @@ def get_binaryen_passes(options): # safe heap must run before post-emscripten, so post-emscripten can apply the sbrk ptr if settings.SAFE_HEAP: passes += ['--safe-heap'] + if settings.EMULATE_FUNCTION_POINTER_CASTS: + # fpcast-emu must run before -Ox so that directize (inside -Ox) sees + # the rewritten table entries with matching types. It must also run + # before asyncify so the byn$fpcast-emu thunks get instrumented. + passes += ['--fpcast-emu'] if optimizing: # wasm-emscripten-finalize will strip the features section for us # automatically, but if we did not modify the wasm then we didn't run it, @@ -372,11 +377,6 @@ def get_binaryen_passes(options): # legalize it again now, as the instrumentation may need it passes += ['--legalize-js-interface'] passes += building.js_legalization_pass_flags() - if settings.EMULATE_FUNCTION_POINTER_CASTS: - # note that this pass must run before asyncify, as if it runs afterwards we only - # generate the byn$fpcast_emu functions after asyncify runs, and so we wouldn't - # be able to further process them. - passes += ['--fpcast-emu'] if settings.ASYNCIFY == 1: passes += ['--asyncify'] if settings.MAIN_MODULE: