Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/wapython
Path: blob/main/core/dylink/src/import.ts
1067 views
1
import stubProxy from "./stub";
2
import debug from "debug";
3
import FunctionTable from "./function-table";
4
import DlopenManager from "./dlopen";
5
import GlobalOffsetTable from "./global-offset-table";
6
import { Env } from "./types";
7
8
const log = debug("dylink");
9
const logImport = debug("dylink:import");
10
11
/*
12
** Our approach to the stack **
13
14
It took me a long time to understand __stack_pointer in WASM and with dynamic
15
linking, since no docs really explained it and I sort of hit a wall with the
16
emscripten sources, since the relevant code appears to be in assembly, and
17
anyway I have to operate in the constraints of what zig provides. The way I
18
understood __stack_pointer is by creating a bunch of sample programs and study
19
the output of wasm-decompile and wasm2wat. First some remarks:
20
21
- the stack size for the main executable is controlled at compile time by zig.
22
It's by default 1MB. I know of no way to create a WebAssembly.Global that
23
is the main __stack_pointer. Maybe emscripten does via assmebly code, but
24
I can't tell, or maybe it just isn't allowed.
25
26
- The stack grows *DOWN*, i.e., if the stack is 1MB then __stack_pointer
27
gets set by default to 1048576 (= 1MB), and at the beginning of each
28
function, the VM grabs the current stack pointer, then substracts off
29
how much space will be needed for running that function, then starts
30
working with that (so it can go up), and then resets it to where it was.
31
32
- Since I have no idea how to get the current __stack_pointer for the main
33
module, here we allocate a new chunk of memory on the heap for each dynamic
34
library to use as its own stack. We then pass in __stack_pointer as a value
35
at the very end of that memory. The __stack_pointer's in each dynamic
36
library and in the module are just completely different variables in different
37
WASM instances that are all using the same shared memory. Everybody has
38
their own stacks and they don't overlap with each other at all. This
39
seems to work well, given our constaints, and hopefully doesn't waste too
40
much memory.
41
42
- Because of this architecture
43
44
NOTE: There are arguments about what stack size to use at the links below. I
45
think it's still 5MB in emscripten today, and in zig it is 1MB:
46
- https://github.com/emscripten-core/emscripten/pull/10019
47
- https://github.com/ziglang/zig/issues/3735 <-- *this issue i reported did get fixed upstream!*
48
*/
49
50
export interface Options {
51
path: string;
52
importObject?: { env?: Env; wasi_snapshot_preview1?: any };
53
importWebAssembly?: (
54
path: string,
55
importObject: object
56
) => Promise<WebAssembly.Instance>;
57
importWebAssemblySync: (
58
path: string,
59
importObject: object
60
) => WebAssembly.Instance;
61
readFileSync: (path: string) => any; // todo?
62
stub?: "warn" | "silent" | false; // if warn, automatically generate stub functions but with a huge warning; if silent, just silently create stubs.
63
allowMainExports?: 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.
64
}
65
66
export default async function importWebAssemblyDlopen({
67
path,
68
importObject,
69
importWebAssembly,
70
importWebAssemblySync,
71
readFileSync,
72
stub,
73
allowMainExports,
74
}: Options): Promise<WebAssembly.Instance> {
75
let mainInstance: WebAssembly.Instance | null = null;
76
if (importObject == null) {
77
importObject = {} as { env?: Partial<Env> };
78
}
79
let { env } = importObject;
80
if (env == null) {
81
env = importObject.env = {};
82
}
83
let { memory } = env;
84
if (memory == null) {
85
memory = env.memory = new WebAssembly.Memory({ initial: 10 });
86
}
87
let { __indirect_function_table } = env;
88
if (__indirect_function_table == null) {
89
// TODO: Make the 1000 bigger if your main module has a large number of function pointers
90
// Maybe we need to parse the wasm bundle in general (that's what emscripten does).
91
// Note that this is only potentially an issue for the main core WASM module, not the
92
// dynamic libraries that get loaded at runtime.
93
__indirect_function_table = env.__indirect_function_table =
94
new WebAssembly.Table({ initial: 1500, element: "anyfunc" });
95
}
96
const functionTable = new FunctionTable(__indirect_function_table);
97
98
function functionViaPointer(key: string) {
99
if (mainInstance == null) return; // not yet available
100
const f = mainInstance.exports[`__WASM_EXPORT__${key}`];
101
if (f == null) return;
102
const ptr = (f as Function)();
103
log("functionViaPointer", key, ptr);
104
return functionTable.get(ptr);
105
}
106
107
function getFunction(
108
name: string,
109
path: string = ""
110
): Function | null | undefined {
111
log("getFunction", name);
112
let f = importObject?.env?.[name];
113
if (f != null) {
114
log("getFunction ", name, "from env");
115
return f;
116
}
117
f = functionViaPointer(name);
118
if (f != null) {
119
log("getFunction ", name, "from function pointer");
120
return f;
121
}
122
f = dlopenManager.getFunction(name);
123
if (f != null) {
124
log("getFunction ", name, "from other library");
125
return f;
126
}
127
128
if (allowMainExports) {
129
/*
130
Any other way of resolving a function needed in a dynamic import that isn't
131
a function pointer is NOT going to work in general:
132
It will segfault or be 1000x too slow. Every function
133
needs to be via a pointer. The following doesn't work *in general*. In addition to
134
speed, there are C functions that make no sense to call via WASM,
135
since they have signatures that are more complicated than WASM supports.
136
*/
137
f = mainInstance?.exports[name];
138
if (f != null) {
139
log(
140
"getFunction ",
141
name,
142
"from mainInstance exports (potentially dangerous!)"
143
);
144
return f;
145
}
146
}
147
148
// **TODO: this is a temporary whitelist for some mangled C++ symbols in the numpy build**
149
if (path?.includes("numpy") && name.startsWith("_Z")) {
150
return () => {
151
console.log("WARNING: calling dangerous stub for ", name);
152
};
153
}
154
155
if (path) {
156
// this is a dynamic library import, so fail at this point:
157
throw Error(`${name} -- undefined when importing ${path}`);
158
}
159
160
return importObjectWithPossibleStub.env[name];
161
}
162
163
function getMainInstance() {
164
if (mainInstance == null) throw Error("bug");
165
return mainInstance;
166
}
167
function getMainInstanceExports() {
168
if (mainInstance?.exports == null) throw Error("bug");
169
return mainInstance.exports;
170
}
171
const globalOffsetTable = new GlobalOffsetTable(
172
getMainInstanceExports,
173
functionTable
174
);
175
176
const dlopenManager = new DlopenManager(
177
getFunction,
178
memory,
179
globalOffsetTable,
180
functionTable,
181
readFileSync,
182
importObject,
183
importWebAssemblySync,
184
getMainInstanceExports,
185
getMainInstance
186
);
187
188
dlopenManager.add_dlmethods(env);
189
190
const importObjectWithPossibleStub = stub
191
? {
192
...importObject,
193
env: stubProxy(importObject.env, functionViaPointer, stub),
194
}
195
: importObject;
196
197
let t0 = 0;
198
if (logImport.enabled) {
199
t0 = new Date().valueOf();
200
logImport("importing ", path);
201
}
202
203
mainInstance =
204
importWebAssembly != null
205
? await importWebAssembly(path, importObjectWithPossibleStub)
206
: importWebAssemblySync(path, importObjectWithPossibleStub);
207
208
if (logImport.enabled) {
209
logImport("imported ", path, ", time =", new Date().valueOf() - t0, "ms");
210
}
211
212
if (mainInstance.exports.__wasm_call_ctors != null) {
213
// We also **MUST** explicitly call the WASM constructors. This is
214
// a library function that is part of the zig libc code. We have
215
// to call this because the wasm file is built using build-lib, so
216
// there is no main that does this. This call does things like
217
// setup the filesystem mapping. Yes, it took me **days**
218
// to figure this out, including reading a lot of assembly code. :shrug:
219
(mainInstance.exports.__wasm_call_ctors as CallableFunction)();
220
}
221
functionTable.updateAfterImport();
222
// TODO
223
(mainInstance as any).env = env;
224
225
(mainInstance as any).getDlopenState = () => {
226
return {
227
dlopen: dlopenManager.getState(),
228
got: globalOffsetTable.getState(),
229
};
230
};
231
232
(mainInstance as any).setDlopenState = (state) => {
233
const { dlopen, got } = state;
234
dlopenManager.setState(dlopen);
235
globalOffsetTable.setState(got);
236
};
237
return mainInstance;
238
}
239
240