From e4037df53b3f2229f5fe46799d5df35fa2d02020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 12 Jan 2022 01:44:18 +0200 Subject: [PATCH] Add support for user JS library code to be able to depend on C functions, via a custom 'foo__wasm_deps: ['wasmFunc1', 'wasmFunc2', ...] directive. --- emcc.py | 6 ++++ src/read_wasm_deps.js | 76 +++++++++++++++++++++++++++++++++++++++++++ src/settings.js | 2 +- tests/test_other.py | 26 +++++++++++++++ tools/deps_info.py | 5 +++ 5 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/read_wasm_deps.js diff --git a/emcc.py b/emcc.py index f4a8f937db979..20be30b54c265 100755 --- a/emcc.py +++ b/emcc.py @@ -35,6 +35,7 @@ from enum import Enum, unique, auto from subprocess import PIPE from urllib.parse import quote +from tools.deps_info import append_wasm_deps_info import emscripten @@ -3749,6 +3750,11 @@ def process_libraries(state, linker_inputs): # Sort the input list from (order, lib_name) pairs to a flat array in the right order. settings.JS_LIBRARIES.sort(key=lambda lib: lib[0]) settings.JS_LIBRARIES = [lib[1] for lib in settings.JS_LIBRARIES] + + for f in settings.JS_LIBRARIES: + wasm_deps = shared.run_js_tool(shared.path_from_root('src/read_wasm_deps.js'), [shared.path_from_root('src', f)], stdout=PIPE) + append_wasm_deps_info(json.loads(wasm_deps)) + state.link_flags = new_flags diff --git a/src/read_wasm_deps.js b/src/read_wasm_deps.js new file mode 100644 index 0000000000000..54bec1d76a4d2 --- /dev/null +++ b/src/read_wasm_deps.js @@ -0,0 +1,76 @@ +var fs = require('fs'); + +function readFile(filename) { + return fs.readFileSync(filename).toString(); +} + +function include(filename) { + eval.call(null, readFile(`${__dirname}/${filename}`)); +} + +include('utility.js'); +include('settings.js'); +include('settings_internal.js'); +include('parseTools.js'); + +// Read all input JS library files into LibraryManager. +var LibraryManager = { library: {} }; +for(let f of process.argv.slice(2)) { + eval(processMacros(preprocess(readFile(f), f))); +} + +// Grab all JS -> JS deps and JS -> C deps from the input files +const DEPS_SUFFIX = '__deps'; +const WASM_DEPS_SUFFIX = '__wasm_deps'; + +let jsToJsDeps = {}; +let jsToWasmDeps = {}; + +for(let s in LibraryManager.library) { + if (s.endsWith(DEPS_SUFFIX)) jsToJsDeps[s.substr(0, s.length - DEPS_SUFFIX.length)] = LibraryManager.library[s]; + else if (s.endsWith(WASM_DEPS_SUFFIX)) jsToWasmDeps[s.substr(0, s.length - WASM_DEPS_SUFFIX.length)] = LibraryManager.library[s]; +} + +// Key jsToJsDeps backwards: given a JS function as key, lists all functions that depend on this function. +let jsToJsBackDeps = {}; + +for(let depender in jsToJsDeps) { + for(let dependee of jsToJsDeps[depender]) { + if (!jsToJsBackDeps[dependee]) jsToJsBackDeps[dependee] = []; + if (!jsToJsBackDeps[dependee].includes(depender)) jsToJsBackDeps[dependee].push(depender); + } +} + +// Appends those elements from array src to array dst that did not yet exist in dst. +// Operates in-place, returns true if any modifications to array dst were done. +function appendToArrayIfNotExists(dst, src) { + let modified = false; + for(let element of src) { + if (!dst.includes(element)) { + dst.push(element); + modified = true; + } + } + return modified; +} + +// Transitively propagate all jsToWasm deps backwards, i.e. if jsFunc1 depends on jsFunc2, +// and jsFunc2 depends on wasmFunc1, also record jsFunc1 to depend on wasmFunc1. +// Perform the propagation to a new dictionary to not disturb iteration over jsToWasmDeps. +let transitiveJsToWasmDeps = {}; +for(let dep in jsToWasmDeps) { + const wasmDeps = jsToWasmDeps[dep]; + let stack = [dep]; + while(stack.length > 0) { + let f = stack.pop(); + if (!transitiveJsToWasmDeps[f]) transitiveJsToWasmDeps[f] = []; + if (appendToArrayIfNotExists(transitiveJsToWasmDeps[f], wasmDeps)) { + // Keep going if this append produced some modifications (this check makes sure we don't infinite loop on cycles) + if (jsToJsBackDeps[f]) stack = stack.concat(jsToJsBackDeps[f]); + } + } +} +jsToWasmDeps = transitiveJsToWasmDeps; + +// Print final output +console.log(JSON.stringify(transitiveJsToWasmDeps)); diff --git a/src/settings.js b/src/settings.js index 9797207944830..dac3294693eae 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1947,7 +1947,7 @@ var SPLIT_MODULE = 0; // llvm-nm on all input) and use the map in deps_info.py to determine // the set of additional dependencies. // 'all' : Include the full set of possible reverse dependencies. -// 'none': No reverse dependences will be added by emscriopten. Any reverse +// 'none': No reverse dependences will be added by emscripten. Any reverse // dependencies will be assumed to be explicitly added to // EXPORTED_FUNCTIONS and deps_info.py will be completely ignored. // While 'auto' will produce a minimal set (so is good for code size), 'all' diff --git a/tests/test_other.py b/tests/test_other.py index 7b950a736c723..76fe55c89cc1a 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -11401,3 +11401,29 @@ def test_gmtime_noleak(self): # Confirm that gmtime_r does not leak when called in isolation. self.emcc_args.append('-fsanitize=leak') self.do_other_test('test_gmtime_noleak.c') + + # Tests the use of foo__wasm_deps: ['wasmFunc1', 'wasmFunc2'] from JS library files + def test_wasm_deps(self): + create_file('lib.js', r''' +mergeInto(LibraryManager.library, { + foo__wasm_deps: ['wasmFunction'], + foo: function() { + return _wasmFunction(); + }, + + bar__deps: ['foo'], + bar: function() { + return _foo(); + } +}); +''') + + create_file('main.c', r''' +#include +int wasmFunction() { return 492; } +int bar(void); +int main() { printf("%d\n", bar()); } +''') + + self.run_process([EMCC, 'main.c', '--js-library', 'lib.js']) + self.assertContained('492', self.run_js('a.out.js')) diff --git a/tools/deps_info.py b/tools/deps_info.py index ad6c7220cd3ac..2a728b0e9601d 100644 --- a/tools/deps_info.py +++ b/tools/deps_info.py @@ -199,6 +199,11 @@ } +def append_wasm_deps_info(wasm_deps_info): + for key, value in wasm_deps_info.items(): + _deps_info[key] = value + + def get_deps_info(): if not settings.EXCEPTION_HANDLING and settings.LINK_AS_CXX: _deps_info['__cxa_begin_catch'] = ['__cxa_is_pointer_type']