Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/wapython
Path: blob/main/core/dylink/src/dlopen.ts
1067 views
1
import { alignMemory, recvString, sendString } from "./util";
2
import { Env, Library, NonMainLibrary } from "./types";
3
import FunctionTable from "./function-table";
4
import GlobalOffsetTable from "./global-offset-table";
5
import debug from "debug";
6
import getMetadata from "./metadata";
7
8
const log = debug("dylink:dlopen");
9
const STACK_ALIGN = 16; // copied from emscripten
10
11
// Stack size for imported dynamic libraries -- we use 1MB. This is
12
// a runtime parameter.
13
const STACK_SIZE = 1048576; // 1MB; to use 64KB it would be 65536.
14
15
export default class DlopenManger {
16
private dlerrorPtr: number = 0;
17
private _malloc?: (number) => number;
18
private _free?: (number) => void;
19
private memory: WebAssembly.Memory;
20
private functionTable: FunctionTable;
21
private globalOffsetTable: GlobalOffsetTable;
22
private pathToLibrary: { [path: string]: Library } = {};
23
private handleToLibrary: { [handle: number]: Library } = {};
24
private readFileSync: (path: string) => Buffer;
25
private importObject: { env?: Env; wasi_snapshot_preview1?: any };
26
private mainGetFunction: (
27
name: string,
28
path?: string
29
) => Function | null | undefined;
30
private importWebAssemblySync: (
31
path: string,
32
importObject: object
33
) => WebAssembly.Instance;
34
private getMainInstanceExports: () => { [key: string]: any };
35
private getMainInstance: () => WebAssembly.Instance;
36
37
constructor(
38
getFunction: (name: string, path?: string) => Function | null | undefined,
39
memory: WebAssembly.Memory,
40
globalOffsetTable: GlobalOffsetTable,
41
functionTable: FunctionTable,
42
readFileSync: (path: string) => Buffer,
43
importObject: { env?: Env; wasi_snapshot_preview1?: any },
44
importWebAssemblySync: (
45
path: string,
46
importObject: object
47
) => WebAssembly.Instance,
48
getMainInstanceExports: () => { [key: string]: any },
49
getMainInstance: () => WebAssembly.Instance
50
) {
51
this.mainGetFunction = getFunction;
52
this.memory = memory;
53
this.globalOffsetTable = globalOffsetTable;
54
this.functionTable = functionTable;
55
this.readFileSync = readFileSync;
56
this.importObject = importObject;
57
this.importWebAssemblySync = importWebAssemblySync;
58
this.getMainInstanceExports = getMainInstanceExports;
59
this.getMainInstance = getMainInstance;
60
}
61
62
add_dlmethods(env: Env) {
63
for (const dlmethod of [
64
"dlopen",
65
"dladdr",
66
"dlclose",
67
"dlerror",
68
"dlsym",
69
]) {
70
env[dlmethod] = this[dlmethod].bind(this);
71
}
72
}
73
74
getState() {
75
const state = new Set<string>();
76
for (const handle in this.handleToLibrary) {
77
state.add(handle);
78
}
79
return state;
80
}
81
82
setState(state) {
83
for (const handle in this.handleToLibrary) {
84
if (!state.has(handle)) {
85
this.dlclose(parseInt(handle));
86
}
87
}
88
}
89
90
private malloc(bytes: number, purpose: string): number {
91
if (this._malloc == null) {
92
const f = this.mainGetFunction("malloc");
93
if (f == null) {
94
throw Error("malloc from libc must be available in the main instance");
95
}
96
this._malloc = f as (number) => number;
97
}
98
const ptr = this._malloc(bytes);
99
if (ptr == 0) {
100
const err = `out of memory -- malloc failed allocating ${purpose}`;
101
log(err);
102
console.warn(err);
103
throw Error(err);
104
}
105
return ptr;
106
}
107
108
private free(ptr: number): void {
109
if (this._free == null) {
110
const f = this.mainGetFunction("free");
111
if (f == null) {
112
throw Error("free from libc must be available in the main instance");
113
}
114
this.free = f as (number) => number;
115
}
116
this.free(ptr);
117
}
118
119
dlopenEnvHandler(path: string) {
120
return (env, key: string) => {
121
if (key in env) {
122
return Reflect.get(env, key);
123
}
124
log("dlopenEnvHandler", key);
125
126
// important to check importObject.env LAST since it could be a proxy
127
// that generates stub functions:
128
const f = this.mainGetFunction(key, path);
129
if (f == null) {
130
log("dlopenEnvHandler got null");
131
return;
132
}
133
return f;
134
// FOR LOW LEVEL DEBUGGING ONLY!
135
// return (...args) => {
136
// console.log("env call ", key);
137
// // @ts-ignore
138
// return f(...args);
139
// };
140
};
141
}
142
143
private symbolViaPointer(name: string) {
144
const exports = this.getMainInstanceExports();
145
if (exports == null) return; // not yet available
146
log("symbolViaPointer", name);
147
let f = exports[`__WASM_EXPORT__${name}`];
148
if (f == null) {
149
return null;
150
}
151
const sym = (f as Function)();
152
log("symbolViaPointer", name, "-->", sym);
153
return sym;
154
}
155
156
dlopen(pathnamePtr: number, _flags: number): number {
157
// TODO: _flags are ignored for now.
158
if (this.memory == null) throw Error("bug"); // mainly for typescript
159
160
// pathnamePtr = null *is* valid and means "the main program", i.e.,
161
// "If filename is NULL, then the returned handle is for the main program."
162
// For example, this null is used by ctypes (in cpython) in the
163
// __init__.py in the line "pythonapi = PyDLL(None)". In all our
164
// code we treat the null pointer the same as path="", for simplicity
165
// of data types (easy in Javascript).
166
167
const path = !pathnamePtr ? "" : recvString(pathnamePtr, this.memory);
168
log("dlopen: path='%s'", path);
169
if (this.pathToLibrary[path] != null) {
170
return this.pathToLibrary[path].handle;
171
}
172
if (!path) {
173
return this.createLibrary({ path, instance: this.getMainInstance() });
174
}
175
176
const binary = new Uint8Array(this.readFileSync(path));
177
const metadata = getMetadata(binary);
178
log("metadata", metadata);
179
// alignments are powers of 2
180
let memAlign = Math.pow(2, metadata.memoryAlign ?? 0);
181
// finalize alignments and verify them
182
memAlign = Math.max(memAlign, STACK_ALIGN); // we at least need stack alignment
183
if (metadata.memorySize == null) {
184
throw Error("memorySize must be defined in the shared library");
185
}
186
const alloc = this.malloc(
187
metadata.memorySize + memAlign,
188
"space for " + path
189
);
190
const stack_alloc = this.malloc(STACK_SIZE, "stack for " + path);
191
192
log(
193
"allocating %s bytes for shared library -- at ",
194
metadata.memorySize + memAlign,
195
alloc
196
);
197
const __memory_base = metadata.memorySize
198
? alignMemory(alloc, memAlign)
199
: 0;
200
const __table_base = metadata.tableSize
201
? this.functionTable.getNextTablePos()
202
: 0;
203
204
const env = {
205
memory: this.memory,
206
__indirect_function_table: this.functionTable.table,
207
__memory_base,
208
__table_base,
209
__stack_pointer: new WebAssembly.Global(
210
{
211
value: "i32",
212
mutable: true,
213
},
214
// This is a pointer to the top of the memory we allocated
215
// for this dynamic library's stack, since the stack grows
216
// down, in terms of memory addresses.
217
stack_alloc + STACK_SIZE
218
),
219
};
220
log("env =", env);
221
const libImportObject = {
222
...this.importObject,
223
env: new Proxy(env, { get: this.dlopenEnvHandler(path) }),
224
"GOT.mem": this.globalOffsetTable.mem,
225
"GOT.func": this.globalOffsetTable.func,
226
};
227
228
// account for the entries that got inserted during the import.
229
// This must happen BEFORE the import, since that will create some
230
// new entries to get put in the table below, and the import itself
231
// will put entries from the current position up to metadata.tableSize
232
// positions forward.
233
if (metadata.tableSize) {
234
this.functionTable.prepareForImport(metadata.tableSize);
235
}
236
237
let t0 = 0;
238
if (log.enabled) {
239
t0 = new Date().valueOf();
240
log("importing ", path);
241
}
242
const instance = this.importWebAssemblySync(path, libImportObject);
243
if (log.enabled) {
244
log("imported ", path, ", time =", new Date().valueOf() - t0, "ms");
245
}
246
247
const symToPtr: { [symName: string]: number } = {};
248
for (const name in instance.exports) {
249
if (this.globalOffsetTable.funcMap[name] != null) continue;
250
const val = instance.exports[name];
251
if (symToPtr[name] != null || typeof val != "function") continue;
252
symToPtr[name] = this.functionTable.set(val as Function);
253
}
254
255
// Set all functions in the function table that couldn't
256
// be resolved to pointers when creating the webassembly module.
257
for (const symName in this.globalOffsetTable.funcMap) {
258
const f =
259
instance.exports[symName] ?? this.getMainInstanceExports()[symName];
260
log(
261
"table[%s] = %s",
262
this.globalOffsetTable.funcMap[symName]?.index,
263
symName,
264
f
265
);
266
if (f == null) {
267
// This has to be a fatal error, since the only other option would
268
// be having a pointer to random nonsense or a broke function,
269
// which is definitely going to segfault randomly later when it
270
// gets hit by running code. See comments above in GOTFuncHandler.
271
throw Error(`dlopen -- UNRESOLVED FUNCTION: ${symName}`);
272
}
273
this.globalOffsetTable.funcMap[symName].set(f as Function);
274
symToPtr[symName] = this.globalOffsetTable.funcMap[symName].index;
275
delete this.globalOffsetTable.funcMap[symName];
276
}
277
const { memMap } = this.globalOffsetTable;
278
for (const symName in memMap) {
279
const x = memMap[symName];
280
delete memMap[symName];
281
const ptrBeforeOffset = (instance.exports[symName] as any)?.value;
282
if (ptrBeforeOffset == null) {
283
const ptr = this.symbolViaPointer(symName);
284
if (ptr == null) {
285
console.error(
286
`dlopen: FATAL ERROR - Symbol '${symName}' is not available in the cowasm kernel or any loaded library via __WASM_EXPORT__${symName} but is required by '${path}'.`
287
);
288
throw Error(`dlopen -- UNRESOLVED SYMBOL: ${symName}`);
289
} else {
290
//console.log("found ", symName, " in global");
291
x.value = ptr;
292
}
293
} else {
294
x.value = ptrBeforeOffset + __memory_base;
295
//console.log("putting ", symName, " in offset");
296
}
297
}
298
299
if (instance.exports.__wasm_call_ctors != null) {
300
// This **MUST** be after updating all the va1
301
log("calling __wasm_call_ctors for dynamic library");
302
(instance.exports.__wasm_call_ctors as CallableFunction)();
303
}
304
305
if (instance.exports.__wasm_apply_data_relocs != null) {
306
// This **MUST** be after updating all the values above!!
307
log("calling __wasm_apply_data_relocs for dynamic library");
308
(instance.exports.__wasm_apply_data_relocs as CallableFunction)();
309
}
310
311
return this.createLibrary({
312
path,
313
instance,
314
symToPtr,
315
stack_alloc,
316
});
317
}
318
319
dlsym(handle: number, symbolPtr: number): number {
320
const symName = recvString(symbolPtr, this.memory);
321
return this._dlsym(handle, symName);
322
}
323
324
private _dlsym(handle: number, symName: string): number {
325
log("_dlsym: handle=%s, symName='%s'", handle, symName);
326
const lib0 = this.handleToLibrary[handle];
327
if (lib0 == null) {
328
throw Error(`dlsym: invalid handle ${handle}`);
329
}
330
331
if (!lib0.path) {
332
// special case -- the the main instance and because of how we run programs, every other library
333
const ptr = (
334
lib0.instance.exports[`__WASM_EXPORT__${symName}`] as any
335
)?.();
336
if (ptr != null) {
337
return ptr;
338
}
339
// now try the others
340
for (const h in this.handleToLibrary) {
341
const handle2 = parseInt(h);
342
if (handle != handle2) {
343
try {
344
return this._dlsym(handle2, symName);
345
} catch (_) {}
346
}
347
}
348
349
// didn't find it
350
this.set_dlerror(`dlsym: handle=${handle} - unknown symbol '${symName}'`);
351
return 0;
352
}
353
354
const lib = lib0 as NonMainLibrary;
355
let ptr = lib.symToPtr[symName];
356
log("sym= ", symName, ", ptr = ", ptr);
357
if (ptr != null) {
358
// symbol is a known function pointer
359
return ptr;
360
}
361
362
// sometimes its an alias:
363
ptr = (lib.instance.exports[`__WASM_EXPORT__${symName}`] as any)?.();
364
if (ptr != null) {
365
// symbol is a known function pointer
366
return ptr;
367
}
368
369
// NOT sure if this is at all correct or meaningful or what to even
370
// do with non functions!
371
// dlsym is supposed to return a null pointer on fail, NOT throw exception
372
this.set_dlerror(`dlsym: handle=${handle} - unknown symbol '${symName}'`);
373
return 0;
374
}
375
376
dladdr() {
377
log("dladdr: NOT IMPLEMENTED");
378
// we couldn't find "it"
379
this.set_dlerror("dladdr is not yet implemented");
380
return 0;
381
}
382
383
/*
384
"The function dlclose() decrements the reference count on the dynamic library
385
handle handle. If the reference count drops to zero and no other loaded
386
libraries use symbols in it, then the dynamic library is unloaded. The function
387
dlclose() returns 0 on success, and nonzero on error."
388
TODO: we do not track "other libraries use symbols in it" yet, so this is very
389
much NOT safe to use if you don't know that it isn't being referenced.
390
*/
391
dlclose(handle: number): number {
392
log("dlclose", handle);
393
const lib0 = this.handleToLibrary[handle];
394
if (lib0 == null) {
395
this.set_dlerror(`dlclose: invalid handle ${handle}`);
396
return 1;
397
}
398
if (!lib0.path) {
399
// it's the main library
400
return 0;
401
}
402
const lib = lib0 as NonMainLibrary;
403
if (lib != null) {
404
for (const name in lib.symToPtr) {
405
const ptr = lib.symToPtr[name];
406
this.functionTable.delete(ptr);
407
}
408
this.free(lib.stack_alloc);
409
// console.log("closing ", lib);
410
delete this.handleToLibrary[handle];
411
delete this.pathToLibrary[lib.path];
412
// need to free the allocated functions.
413
}
414
return 0;
415
}
416
417
set_dlerror(s: string) {
418
if (!this.dlerrorPtr) {
419
// allocate space for the error
420
this.dlerrorPtr = this.malloc(1024, "dlerror pointer");
421
}
422
sendString(s.slice(0, 1023), this.dlerrorPtr, this.memory);
423
}
424
/*
425
"The function dlerror() returns a human readable string describing the most
426
recent error that occurred from dlopen(), dlsym() or dlclose() since the last
427
call to dlerror(). It returns NULL if no errors have occurred since
428
initialization or since it was last called."
429
*/
430
dlerror() {
431
return this.dlerrorPtr;
432
}
433
434
// See if the function we want is defined in some
435
// already imported dynamic library:
436
getFunction(name: string): Function | null | undefined {
437
for (const handle in this.handleToLibrary) {
438
const { path, symToPtr, instance } = this.handleToLibrary[handle];
439
// two places that could have the pointer:
440
const ptr =
441
symToPtr?.[name] ??
442
(instance.exports[`__WASM_EXPORT__${name}`] as Function)?.();
443
if (ptr != null) {
444
log("getFunction", name, path, "handle=", handle);
445
return this.functionTable.get(ptr);
446
}
447
}
448
return undefined;
449
}
450
451
createLibrary({
452
path,
453
instance,
454
symToPtr,
455
stack_alloc,
456
}: PartialBy<Library, 'handle'>): number {
457
// Get an available handle by maxing all the int versions of the
458
// keys of the handleToLibrary map.
459
const handle =
460
Math.max(
461
0,
462
...Object.keys(this.handleToLibrary).map((n) => parseInt(n))
463
) + 1;
464
465
const library = { path, handle, instance, symToPtr, stack_alloc };
466
this.pathToLibrary[path] = library;
467
this.handleToLibrary[handle] = library;
468
return handle;
469
}
470
}
471
// https://stackoverflow.com/questions/43159887/make-a-single-property-optional-in-typescript
472
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
473
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
474
475