Skip to content
File

Blob: src/pyodide/internal/pool/emscriptenSetup.ts

typescript254 lines
1/**
2 * This file is intended to be executed in the Python pool (once it exists). As such, it cannot
3 * import anything that transitively uses C++ extension modules. It has to work in a vanilla v8
4 * isolate. Also, we will have to bundle this file and all of its transitive imports into a single
5 * js file.
6 */
7 
8/**
9 * _createPyodideModule and pyodideWasmModule together are produced by the
10 * Emscripten linker
11 */
12import { _createPyodideModule } from 'pyodide-internal:generated/pyodide.asm';
13 
14import {
15 setGetRandomValues,
16 setSetTimeout,
17 finishSetup,
18} from 'pyodide-internal:pool/builtin_wrappers';
19 
20import { getSentinelImport } from 'pyodide-internal:pool/sentinel';
21 
22/**
23 * A preRun hook. Make sure environment variables are visible at runtime.
24 */
25function setEnv(Module: Module): void {
26 Object.assign(Module.ENV, Module.API.config.env);
27}
28 
29function getWaitForDynlibs(resolveReadyPromise: PreRunHook): PreRunHook {
30 return function waitForDynlibs(Module: Module): void {
31 // Block the instantiation of the runtime until we can preload the dynamic libraries. The
32 // promise returned by _createPyodideModule won't resolve until we call
33 // `removeRunDependency('dynlibs')` so we use `emscriptenSettings.readyPromise` to continue
34 // execution when we've gotten to this point.
35 Module.addRunDependency('dynlibs');
36 resolveReadyPromise(Module);
37 };
38}
39 
40function computeVersionTuple(Module: Module): [number, number, number] {
41 if (Module._py_version_major) {
42 const pymajor = Module._py_version_major();
43 const pyminor = Module._py_version_minor();
44 const micro = Module._py_version_micro();
45 return [pymajor, pyminor, micro];
46 }
47 const versionInt = Module.HEAPU32[Module._Py_Version >>> 2];
48 const major = (versionInt >>> 24) & 0xff;
49 const minor = (versionInt >>> 16) & 0xff;
50 const micro = (versionInt >>> 8) & 0xff;
51 return [major, minor, micro];
52}
53 
54/**
55 * This is passed as a preRun hook in EmscriptenSettings, run just before
56 * main(). It ensures that the file system includes the stuff that main() needs,
57 * most importantly the Python standard library.
58 *
59 * Put the Python + Pyodide standard libraries into a zip file in the
60 * appropriate location /lib/python311.zip . Python will import stuff directly
61 * from this zip file using ZipImporter.
62 *
63 * ZipImporter is quite useful here -- the Python runtime knows how to unpack a
64 * bunch of different archive formats but it is not possible to use these until
65 * the runtime state is initialized. So ZipImporter breaks this bootstrapping
66 * knot for us.
67 *
68 * We also make an empty home directory and an empty global site-packages
69 * directory `/lib/pythonv.vv/site-packages`.
70 *
71 * This is a simplified version of the `prepareFileSystem` function here:
72 * https://github.com/pyodide/pyodide/blob/main/src/js/module.ts
73 */
74function getPrepareFileSystem(pythonStdlib: ArrayBuffer): PreRunHook {
75 return function prepareFileSystem(Module: Module): void {
76 Module.API.pyVersionTuple = computeVersionTuple(Module);
77 const [pymajor, pyminor] = Module.API.pyVersionTuple;
78 Module.FS.sitePackages = `/lib/python${pymajor}.${pyminor}/site-packages`;
79 Module.LD_LIBRARY_PATH = [
80 '/usr/lib',
81 Module.FS.sitePackages,
82 '/session/metadata/python_modules/lib/',
83 ].join(':');
84 Module.ENV.LD_LIBRARY_PATH = Module.LD_LIBRARY_PATH;
85 Module.FS.sessionSitePackages = '/session' + Module.FS.sitePackages;
86 Module.FS.mkdirTree(Module.FS.sitePackages);
87 Module.FS.writeFile(
88 `/lib/python${pymajor}${pyminor}.zip`,
89 new Uint8Array(pythonStdlib),
90 { canOwn: true }
91 );
92 Module.FS.mkdirTree(Module.API.config.env.HOME);
93 };
94}
95 
96/**
97 * A hook that the Emscripten runtime calls to perform the WebAssembly
98 * instantiation action. Once instantiated, this callback function should call
99 * ``successCallback()`` with the generated WebAssembly Instance object.
100 *
101 * @param wasmImports a JS object which contains all the function imports that
102 * need to be passed to the WebAssembly Module when instantiating
103 * @param successCallback A callback to indicate that instantiation was
104 * successful,
105 * @returns The return value of this function should contain the ``exports`` object of
106 * the instantiated WebAssembly Module, or an empty dictionary object ``{}`` if
107 * the instantiation is performed asynchronously, or ``false`` if instantiation
108 * synchronously failed. There is no way to indicate asynchronous failure.
109 */
110function getInstantiateWasm(
111 pyodideWasmModule: WebAssembly.Module
112): EmscriptenSettings['instantiateWasm'] {
113 const sentinelImportPromise = getSentinelImport();
114 return function instantiateWasm(
115 wasmImports: WebAssembly.Imports,
116 successCallback: (
117 inst: WebAssembly.Instance,
118 mod: WebAssembly.Module
119 ) => void
120 ): WebAssembly.Exports {
121 (async function (): Promise<void> {
122 wasmImports.sentinel = await sentinelImportPromise;
123 // Instantiate pyodideWasmModule with wasmImports
124 const instance = await WebAssembly.instantiate(
125 pyodideWasmModule,
126 wasmImports
127 );
128 successCallback(instance, pyodideWasmModule);
129 })().catch((e: unknown) => {
130 console.error(
131 'Internal error: wasm instantiation failed. This should never happen.',
132 e
133 );
134 // Execution hangs at this point.
135 });
136 
137 return {};
138 };
139}
140 
141/**
142 * The Emscripten settings object
143 *
144 * This isn't public API of Pyodide so it's a bit fiddly.
145 */
146function getEmscriptenSettings(
147 isWorkerd: boolean,
148 pythonStdlib: ArrayBuffer,
149 pyodideWasmModule: WebAssembly.Module
150): EmscriptenSettings {
151 const config: PyodideConfig = {
152 // jsglobals is used for the js module.
153 jsglobals: globalThis,
154 // environment variables go here
155 env: {
156 HOME: '/session',
157 // We don't have access to entropy at startup so we cannot support hash
158 // randomization. Setting `PYTHONHASHSEED` disables it. See further
159 // discussion in topLevelEntropy/entropy_patches.py
160 PYTHONHASHSEED: '111',
161 },
162 lockFileURL: '',
163 enableRunUntilComplete: true,
164 };
165 let lockFilePromise;
166 if (isWorkerd) {
167 lockFilePromise = new Promise(
168 (res) => (config.resolveLockFilePromise = res)
169 );
170 }
171 const API = { config, lockFilePromise };
172 let resolveReadyPromise: (mod: Module) => void;
173 let rejectReadyPromise: (e: any) => void = () => {};
174 const readyPromise: Promise<Module> = new Promise((res, rej) => {
175 resolveReadyPromise = res;
176 rejectReadyPromise = rej;
177 });
178 const waitForDynlibs = getWaitForDynlibs(resolveReadyPromise!);
179 const prepareFileSystem = getPrepareFileSystem(pythonStdlib);
180 const instantiateWasm = getInstantiateWasm(pyodideWasmModule);
181 
182 // Emscripten settings to control runtime instantiation.
183 return {
184 // preRun hook to set up the file system before running main
185 // The preRun hook gets run independently of noInitialRun, which is
186 // important because the file system lives outside of linear memory.
187 preRun: [prepareFileSystem, setEnv, waitForDynlibs],
188 instantiateWasm,
189 reportUndefinedSymbolsNoOp(): void {},
190 readyPromise,
191 rejectReadyPromise,
192 API, // Pyodide requires we pass this in.
193 };
194}
195 
196/**
197 * Force Emscripten to feature detect the way we want.
198 * We want it to think we're the browser main thread.
199 */
200/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
201function* featureDetectionMonkeyPatchesContextManager(): Generator<void> {
202 const global = globalThis as any;
203 // Make Emscripten think we're in the browser main thread
204 global.window = { sessionStorage: {} };
205 global.document = { createElement(): void {} };
206 global.sessionStorage = {};
207 // Make Emscripten think we're not in a worker
208 global.importScripts = 1;
209 global.WorkerGlobalScope = undefined;
210 try {
211 yield;
212 } finally {
213 delete global.window;
214 delete global.document;
215 delete global.sessionStorage;
216 delete global.importScripts;
217 }
218}
219/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
220 
221/**
222 * Simple wrapper around _createPyodideModule that applies some monkey patches
223 * to force the environment to be detected the way we want.
224 *
225 * In the long run we should fix this in `pyodide.asm.js` instead.
226 *
227 * Returns the instantiated emscriptenModule object.
228 */
229export async function instantiateEmscriptenModule(
230 isWorkerd: boolean,
231 pythonStdlib: ArrayBuffer,
232 wasmModule: WebAssembly.Module
233): Promise<Module> {
234 const emscriptenSettings = getEmscriptenSettings(
235 isWorkerd,
236 pythonStdlib,
237 wasmModule
238 );
239 for (const _ of featureDetectionMonkeyPatchesContextManager()) {
240 // Ignore the returned promise, it won't resolve until we're done preloading dynamic
241 // libraries.
242 const _promise = _createPyodideModule(emscriptenSettings).catch((e) =>
243 emscriptenSettings.rejectReadyPromise(e)
244 );
245 }
246 
247 // Wait until we've executed all the preRun hooks before proceeding
248 const emscriptenModule = await emscriptenSettings.readyPromise;
249 emscriptenModule.setGetRandomValues = setGetRandomValues;
250 emscriptenModule.setSetTimeout = setSetTimeout;
251 finishSetup();
252 return emscriptenModule;
253}