import stubProxy from "./stub";1import debug from "debug";2import FunctionTable from "./function-table";3import DlopenManager from "./dlopen";4import GlobalOffsetTable from "./global-offset-table";5import { Env } from "./types";67const log = debug("dylink");8const logImport = debug("dylink:import");910/*11** Our approach to the stack **1213It took me a long time to understand __stack_pointer in WASM and with dynamic14linking, since no docs really explained it and I sort of hit a wall with the15emscripten sources, since the relevant code appears to be in assembly, and16anyway I have to operate in the constraints of what zig provides. The way I17understood __stack_pointer is by creating a bunch of sample programs and study18the output of wasm-decompile and wasm2wat. First some remarks:1920- the stack size for the main executable is controlled at compile time by zig.21It's by default 1MB. I know of no way to create a WebAssembly.Global that22is the main __stack_pointer. Maybe emscripten does via assmebly code, but23I can't tell, or maybe it just isn't allowed.2425- The stack grows *DOWN*, i.e., if the stack is 1MB then __stack_pointer26gets set by default to 1048576 (= 1MB), and at the beginning of each27function, the VM grabs the current stack pointer, then substracts off28how much space will be needed for running that function, then starts29working with that (so it can go up), and then resets it to where it was.3031- Since I have no idea how to get the current __stack_pointer for the main32module, here we allocate a new chunk of memory on the heap for each dynamic33library to use as its own stack. We then pass in __stack_pointer as a value34at the very end of that memory. The __stack_pointer's in each dynamic35library and in the module are just completely different variables in different36WASM instances that are all using the same shared memory. Everybody has37their own stacks and they don't overlap with each other at all. This38seems to work well, given our constaints, and hopefully doesn't waste too39much memory.4041- Because of this architecture4243NOTE: There are arguments about what stack size to use at the links below. I44think it's still 5MB in emscripten today, and in zig it is 1MB:45- https://github.com/emscripten-core/emscripten/pull/1001946- https://github.com/ziglang/zig/issues/3735 <-- *this issue i reported did get fixed upstream!*47*/4849export interface Options {50path: string;51importObject?: { env?: Env; wasi_snapshot_preview1?: any };52importWebAssembly?: (53path: string,54importObject: object55) => Promise<WebAssembly.Instance>;56importWebAssemblySync: (57path: string,58importObject: object59) => WebAssembly.Instance;60readFileSync: (path: string) => any; // todo?61stub?: "warn" | "silent" | false; // if warn, automatically generate stub functions but with a huge warning; if silent, just silently create stubs.62allowMainExports?: boolean; // DANGEROUS -- allow dll to use functions defined in the main module that are NOT exported via the function table. This is dangerous since they are 1000x slower, and might not be posisble to properly call (depending on data types). Use with caution.63}6465export default async function importWebAssemblyDlopen({66path,67importObject,68importWebAssembly,69importWebAssemblySync,70readFileSync,71stub,72allowMainExports,73}: Options): Promise<WebAssembly.Instance> {74let mainInstance: WebAssembly.Instance | null = null;75if (importObject == null) {76importObject = {} as { env?: Partial<Env> };77}78let { env } = importObject;79if (env == null) {80env = importObject.env = {};81}82let { memory } = env;83if (memory == null) {84memory = env.memory = new WebAssembly.Memory({ initial: 10 });85}86let { __indirect_function_table } = env;87if (__indirect_function_table == null) {88// TODO: Make the 1000 bigger if your main module has a large number of function pointers89// Maybe we need to parse the wasm bundle in general (that's what emscripten does).90// Note that this is only potentially an issue for the main core WASM module, not the91// dynamic libraries that get loaded at runtime.92__indirect_function_table = env.__indirect_function_table =93new WebAssembly.Table({ initial: 1500, element: "anyfunc" });94}95const functionTable = new FunctionTable(__indirect_function_table);9697function functionViaPointer(key: string) {98if (mainInstance == null) return; // not yet available99const f = mainInstance.exports[`__WASM_EXPORT__${key}`];100if (f == null) return;101const ptr = (f as Function)();102log("functionViaPointer", key, ptr);103return functionTable.get(ptr);104}105106function getFunction(107name: string,108path: string = ""109): Function | null | undefined {110log("getFunction", name);111let f = importObject?.env?.[name];112if (f != null) {113log("getFunction ", name, "from env");114return f;115}116f = functionViaPointer(name);117if (f != null) {118log("getFunction ", name, "from function pointer");119return f;120}121f = dlopenManager.getFunction(name);122if (f != null) {123log("getFunction ", name, "from other library");124return f;125}126127if (allowMainExports) {128/*129Any other way of resolving a function needed in a dynamic import that isn't130a function pointer is NOT going to work in general:131It will segfault or be 1000x too slow. Every function132needs to be via a pointer. The following doesn't work *in general*. In addition to133speed, there are C functions that make no sense to call via WASM,134since they have signatures that are more complicated than WASM supports.135*/136f = mainInstance?.exports[name];137if (f != null) {138log(139"getFunction ",140name,141"from mainInstance exports (potentially dangerous!)"142);143return f;144}145}146147// **TODO: this is a temporary whitelist for some mangled C++ symbols in the numpy build**148if (path?.includes("numpy") && name.startsWith("_Z")) {149return () => {150console.log("WARNING: calling dangerous stub for ", name);151};152}153154if (path) {155// this is a dynamic library import, so fail at this point:156throw Error(`${name} -- undefined when importing ${path}`);157}158159return importObjectWithPossibleStub.env[name];160}161162function getMainInstance() {163if (mainInstance == null) throw Error("bug");164return mainInstance;165}166function getMainInstanceExports() {167if (mainInstance?.exports == null) throw Error("bug");168return mainInstance.exports;169}170const globalOffsetTable = new GlobalOffsetTable(171getMainInstanceExports,172functionTable173);174175const dlopenManager = new DlopenManager(176getFunction,177memory,178globalOffsetTable,179functionTable,180readFileSync,181importObject,182importWebAssemblySync,183getMainInstanceExports,184getMainInstance185);186187dlopenManager.add_dlmethods(env);188189const importObjectWithPossibleStub = stub190? {191...importObject,192env: stubProxy(importObject.env, functionViaPointer, stub),193}194: importObject;195196let t0 = 0;197if (logImport.enabled) {198t0 = new Date().valueOf();199logImport("importing ", path);200}201202mainInstance =203importWebAssembly != null204? await importWebAssembly(path, importObjectWithPossibleStub)205: importWebAssemblySync(path, importObjectWithPossibleStub);206207if (logImport.enabled) {208logImport("imported ", path, ", time =", new Date().valueOf() - t0, "ms");209}210211if (mainInstance.exports.__wasm_call_ctors != null) {212// We also **MUST** explicitly call the WASM constructors. This is213// a library function that is part of the zig libc code. We have214// to call this because the wasm file is built using build-lib, so215// there is no main that does this. This call does things like216// setup the filesystem mapping. Yes, it took me **days**217// to figure this out, including reading a lot of assembly code. :shrug:218(mainInstance.exports.__wasm_call_ctors as CallableFunction)();219}220functionTable.updateAfterImport();221// TODO222(mainInstance as any).env = env;223224(mainInstance as any).getDlopenState = () => {225return {226dlopen: dlopenManager.getState(),227got: globalOffsetTable.getState(),228};229};230231(mainInstance as any).setDlopenState = (state) => {232const { dlopen, got } = state;233dlopenManager.setState(dlopen);234globalOffsetTable.setState(got);235};236return mainInstance;237}238239240