pyembed 0.24.0

Embed a Python interpreter
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! Manage an embedded Python interpreter.

use {
    crate::{
        config::{OxidizedPythonInterpreterConfig, ResolvedOxidizedPythonInterpreterConfig},
        conversion::osstring_to_bytes,
        error::NewInterpreterError,
        osutils::resolve_terminfo_dirs,
        pyalloc::PythonMemoryAllocator,
    },
    once_cell::sync::Lazy,
    oxidized_importer::{
        install_path_hook, remove_external_importers, replace_meta_path_importers, ImporterState,
        OxidizedFinder, PyInit_oxidized_importer, PythonResourcesState, OXIDIZED_IMPORTER_NAME,
        OXIDIZED_IMPORTER_NAME_STR,
    },
    pyo3::{
        exceptions::PyRuntimeError, ffi as pyffi, prelude::*, types::PyDict, AsPyPointer,
        PyTypeInfo,
    },
    python_packaging::interpreter::{MultiprocessingStartMethod, TerminfoResolution},
    std::{
        collections::BTreeSet,
        env, fs,
        io::Write,
        os::raw::c_char,
        path::{Path, PathBuf},
    },
};

static GLOBAL_INTERPRETER_GUARD: Lazy<std::sync::Mutex<()>> =
    Lazy::new(|| std::sync::Mutex::new(()));

/// Manages an embedded Python interpreter.
///
/// Python interpreters have global state and there can only be a single
/// instance of this type per process. There exists a global lock enforcing
/// this. Calling `new()` will block waiting for this lock. The lock is
/// released when the instance is dropped.
///
/// Instances must only be constructed through [`MainPythonInterpreter::new()`](#method.new).
///
/// This type and its various functionality is a glorified wrapper around the
/// Python C API. But there's a lot of added functionality on top of what the C
/// API provides.
///
/// # Usage
///
/// Construct instances via [MainPythonInterpreter::new()]. This will acquire
/// a global lock and initialize the main Python interpreter in the current
/// process.
///
/// Python code can then be executed in the interpreter in any number of
/// different ways.
///
/// If you want to run whatever was configured to run via the
/// [OxidizedPythonInterpreterConfig] used to construct the instance, call
/// [MainPythonInterpreter::run()] or [MainPythonInterpreter::py_runmain()].
/// The former will honor "multiprocessing worker" and is necessary for
/// `multiprocessing` to work. [MainPythonInterpreter::py_runmain()] bypasses
/// multiprocessing mode checks.
///
/// If you want to execute arbitrary Python code or want to run Rust code
/// with the GIL held, call [MainPythonInterpreter::with_gil()]. The provided
/// function will be provided a [pyo3::Python], which represents a handle on
/// the Python interpreter. This function is just a wrapper around
/// [pyo3::Python::with_gil()]. But since the function holds a reference to
/// self, it prevents [MainPythonInterpreter] from being dropped prematurely.
///
/// # Safety
///
/// Dropping a [MainPythonInterpreter] instance will call `Py_FinalizeEx()` to
/// finalize the Python interpreter and prevent it from running any more Python
/// code.
///
/// If a Python C API is called after interpreter finalization, a segfault can
/// occur.
///
/// If you use pyo3 APIs like [Python::with_gil()] directly, you may
/// inadvertently attempt to operate on a finalized interpreter. Therefore
/// it is recommended to always go through a method on an [MainPythonInterpreter]
/// instance in order to interact with the Python interpreter.
pub struct MainPythonInterpreter<'interpreter, 'resources: 'interpreter> {
    // It is possible to have a use-after-free if config is dropped before the
    // interpreter is finalized/dropped.
    config: ResolvedOxidizedPythonInterpreterConfig<'resources>,
    interpreter_guard: Option<std::sync::MutexGuard<'interpreter, ()>>,
    pub(crate) allocator: Option<PythonMemoryAllocator>,
    /// File to write containing list of modules when the interpreter finalizes.
    write_modules_path: Option<PathBuf>,
}

impl<'interpreter, 'resources> MainPythonInterpreter<'interpreter, 'resources> {
    /// Construct a Python interpreter from a configuration.
    ///
    /// The Python interpreter is initialized as a side-effect. The GIL is held.
    pub fn new(
        config: OxidizedPythonInterpreterConfig<'resources>,
    ) -> Result<MainPythonInterpreter<'interpreter, 'resources>, NewInterpreterError> {
        let config: ResolvedOxidizedPythonInterpreterConfig<'resources> = config.try_into()?;

        match config.terminfo_resolution {
            TerminfoResolution::Dynamic => {
                if let Some(v) = resolve_terminfo_dirs() {
                    env::set_var("TERMINFO_DIRS", v);
                }
            }
            TerminfoResolution::Static(ref v) => {
                env::set_var("TERMINFO_DIRS", v);
            }
            TerminfoResolution::None => {}
        }

        let mut res = MainPythonInterpreter {
            config,
            interpreter_guard: None,
            allocator: None,
            write_modules_path: None,
        };

        res.init()?;

        Ok(res)
    }

    /// Initialize the interpreter.
    ///
    /// This mutates global state in the Python interpreter according to the
    /// bound config and initializes the Python interpreter.
    ///
    /// After this is called, the embedded Python interpreter is ready to
    /// execute custom code.
    ///
    /// If called more than once, the function is a no-op from the perspective
    /// of interpreter initialization.
    ///
    /// The GIL is not held after the interpreter is initialized.
    fn init(&mut self) -> Result<(), NewInterpreterError> {
        assert!(self.interpreter_guard.is_none());
        self.interpreter_guard = Some(GLOBAL_INTERPRETER_GUARD.lock().map_err(|_| {
            NewInterpreterError::Simple("unable to acquire global interpreter guard")
        })?);

        if let Some(tcl_library) = &self.config.tcl_library {
            std::env::set_var("TCL_LIBRARY", tcl_library);
        }

        set_pyimport_inittab(&self.config);

        // Pre-configure Python.
        let pre_config = pyffi::PyPreConfig::try_from(&self.config)?;

        unsafe {
            let status = pyffi::Py_PreInitialize(&pre_config);

            if pyffi::PyStatus_Exception(status) != 0 {
                return Err(NewInterpreterError::new_from_pystatus(
                    &status,
                    "Python pre-initialization",
                ));
            }
        };

        // Set the memory allocator domains if they are configured.
        self.allocator = PythonMemoryAllocator::from_backend(self.config.allocator_backend);

        if let Some(allocator) = &self.allocator {
            if self.config.allocator_raw {
                allocator.set_allocator(pyffi::PyMemAllocatorDomain::PYMEM_DOMAIN_RAW);
            }

            if self.config.allocator_mem {
                allocator.set_allocator(pyffi::PyMemAllocatorDomain::PYMEM_DOMAIN_MEM);
            }

            if self.config.allocator_obj {
                allocator.set_allocator(pyffi::PyMemAllocatorDomain::PYMEM_DOMAIN_OBJ);
            }

            if self.config.allocator_pymalloc_arena {
                if self.config.allocator_mem || self.config.allocator_obj {
                    return Err(NewInterpreterError::Simple("A custom pymalloc arena allocator cannot be used with custom `mem` or `obj` domain allocators"));
                }

                allocator.set_arena_allocator();
            }
        }

        // Debug hooks apply to all allocator domains and work with or without
        // custom domain allocators.
        if self.config.allocator_debug {
            unsafe {
                pyffi::PyMem_SetupDebugHooks();
            }
        }

        let mut py_config: pyffi::PyConfig = (&self.config).try_into()?;

        // Enable multi-phase initialization. This allows us to initialize
        // our custom importer before Python attempts any imports.
        py_config._init_main = 0;

        let status = unsafe { pyffi::Py_InitializeFromConfig(&py_config) };
        if unsafe { pyffi::PyStatus_Exception(status) } != 0 {
            return Err(NewInterpreterError::new_from_pystatus(
                &status,
                "initializing Python core",
            ));
        }

        // The GIL is held.
        debug_assert_eq!(unsafe { pyffi::PyGILState_Check() }, 1);

        // At this point, the core of Python is initialized.
        // importlib._bootstrap has been loaded. But not
        // importlib._bootstrap_external. This is where we work our magic to
        // inject our custom importer.

        let oxidized_finder_loaded =
            unsafe { Python::with_gil_unchecked(|py| self.inject_oxidized_importer(py))? };

        // The GIL is still held after calling into PyO3.
        debug_assert_eq!(unsafe { pyffi::PyGILState_Check() }, 1);

        // Now proceed with the Python main initialization. This will initialize
        // importlib. And if the custom importlib bytecode was registered above,
        // our extension module will get imported and initialized.
        let status = unsafe { pyffi::_Py_InitializeMain() };
        if unsafe { pyffi::PyStatus_Exception(status) } != 0 {
            return Err(NewInterpreterError::new_from_pystatus(
                &status,
                "initializing Python main",
            ));
        }

        // The GIL is held after finishing initialization.
        debug_assert_eq!(unsafe { pyffi::PyGILState_Check() }, 1);

        // We release the GIL so we can have pyo3's GIL handling take over from
        // an "empty" state. This mirrors what pyo3's prepare_freethreaded_python() does.
        unsafe {
            pyffi::PyEval_SaveThread();
        }

        self.write_modules_path =
            self.with_gil(|py| self.init_post_main(py, oxidized_finder_loaded))?;

        debug_assert_eq!(unsafe { pyffi::PyGILState_Check() }, 0);

        Ok(())
    }

    /// Inject OxidizedFinder into Python's importing mechanism.
    ///
    /// This function is meant to be called as part of multi-phase interpreter initialization
    /// after `Py_InitializeFromConfig()` but before `_Py_InitializeMain()`. Calling it
    /// any other time may result in errors.
    ///
    /// Returns whether an `OxidizedFinder` was injected into the interpreter.
    fn inject_oxidized_importer(&self, py: Python) -> Result<bool, NewInterpreterError> {
        if !self.config.oxidized_importer {
            return Ok(false);
        }

        let resources_state = Box::new(PythonResourcesState::try_from(&self.config)?);

        let oxidized_importer = py.import(OXIDIZED_IMPORTER_NAME_STR).map_err(|err| {
            NewInterpreterError::new_from_pyerr(py, err, "import of oxidized importer module")
        })?;

        let cb = |importer_state: &mut ImporterState| match self.config.multiprocessing_start_method
        {
            MultiprocessingStartMethod::None => {}
            MultiprocessingStartMethod::Fork
            | MultiprocessingStartMethod::ForkServer
            | MultiprocessingStartMethod::Spawn => {
                importer_state.set_multiprocessing_set_start_method(Some(
                    self.config.multiprocessing_start_method.to_string(),
                ));
            }
            MultiprocessingStartMethod::Auto => {
                // Windows uses "spawn" because "fork" isn't available.
                // Everywhere else uses "fork." The default on macOS is "spawn." This
                // is due to https://bugs.python.org/issue33725, which only affects
                // Python framework builds. Our assumption is we aren't using a Python
                // framework, so "spawn" is safe.
                let method = if cfg!(target_family = "windows") {
                    "spawn"
                } else {
                    "fork"
                };

                importer_state.set_multiprocessing_set_start_method(Some(method.to_string()));
            }
        };

        // Ownership of the resources state is transferred into the importer, where the Box
        // is summarily leaked. However, the importer tracks a pointer to the resources state
        // and will constitute the struct for dropping when it itself is dropped. We could
        // potentially encounter a use-after-free if the importer is used after self.config
        // is dropped. However, that would require self to be dropped. And if self is dropped,
        // there should no longer be a Python interpreter around. So it follows that the
        // importer state cannot be dropped after self.

        replace_meta_path_importers(py, oxidized_importer, resources_state, Some(cb)).map_err(
            |err| {
                NewInterpreterError::new_from_pyerr(py, err, "initialization of oxidized importer")
            },
        )?;

        Ok(true)
    }

    /// Performs interpreter configuration after main interpreter initialization.
    fn init_post_main(
        &self,
        py: Python,
        oxidized_finder_loaded: bool,
    ) -> Result<Option<PathBuf>, NewInterpreterError> {
        let sys_module = py
            .import("sys")
            .map_err(|e| NewInterpreterError::new_from_pyerr(py, e, "obtaining sys module"))?;

        // When the main initialization ran, it initialized the "external"
        // importer (importlib._bootstrap_external), mutating `sys.meta_path`
        // and `sys.path_hooks`.
        //
        // We normally expect `OxidizedFinder` to be the initial entry on `sys.meta_path`,
        // as that is where we place it. And if it were capable, `OxidizedFinder` would
        // have serviced all imports so far.
        //
        // However, initialization of the stdlib external importer could result in
        // additional mutations to `sys.meta_path` and `sys.path_hooks`. For example,
        // if `.pth` files are being processed by the import of `site`, a `.pth` file
        // could inject its own importers. This is commonly seen with the
        // `_distutils_hack` meta path importer provided by `setuptools`.
        //
        // Here, we undo the mutations caused by initializing of the "external" importers if
        // we're not configured to perform filesystem importing. Ideally there would be a
        // field on `PyConfig` to prevent the initializing of these importers. But there isn't.
        // There is an `_install_importlib` field. However, when disabled it disables a lot of
        // "main" initialization and isn't usable for us.
        //
        // TODO consider importing `site` ourselves instead of letting the built-in init code
        // do it. This should give us even more control over importer handling. It is unknown
        // whether it is safe to defer the import of this module post completion of
        // _Py_InitializeMain.

        if !self.config.filesystem_importer {
            remove_external_importers(sys_module).map_err(|err| {
                NewInterpreterError::new_from_pyerr(py, err, "removing external importers")
            })?;
        }

        // We aren't able to hold a &PyAny to OxidizedFinder through multi-phase interpreter
        // initialization. So recover an instance now if it is available.
        let oxidized_finder = if oxidized_finder_loaded {
            sys_module
                .getattr("meta_path")
                .map_err(|err| {
                    NewInterpreterError::new_from_pyerr(py, err, "obtaining sys.meta_path")
                })?
                .iter()
                .map_err(|err| {
                    NewInterpreterError::new_from_pyerr(
                        py,
                        err,
                        "obtaining iterator for sys.meta_path",
                    )
                })?
                .find(|finder| {
                    // This should never fail.
                    if let Ok(finder) = finder {
                        OxidizedFinder::is_type_of(finder)
                    } else {
                        false
                    }
                })
        } else {
            None
        };

        if let Some(Ok(finder)) = oxidized_finder {
            install_path_hook(finder, sys_module).map_err(|err| {
                NewInterpreterError::new_from_pyerr(
                    py,
                    err,
                    "installing OxidizedFinder in sys.path_hooks",
                )
            })?;
        }

        if self.config.argvb {
            let args_objs = self
                .config
                .resolve_sys_argvb()
                .iter()
                .map(|x| osstring_to_bytes(py, x.clone()))
                .collect::<Vec<_>>();

            let args = args_objs.to_object(py);
            let argvb = b"argvb\0";

            let res =
                unsafe { pyffi::PySys_SetObject(argvb.as_ptr() as *const c_char, args.as_ptr()) };

            match res {
                0 => (),
                _ => return Err(NewInterpreterError::Simple("unable to set sys.argvb")),
            }
        }

        // As a convention, sys.oxidized is set to indicate we are running from
        // a self-contained application.
        let oxidized = b"oxidized\0";
        let py_true = true.into_py(py);

        let res =
            unsafe { pyffi::PySys_SetObject(oxidized.as_ptr() as *const c_char, py_true.as_ptr()) };

        match res {
            0 => (),
            _ => return Err(NewInterpreterError::Simple("unable to set sys.oxidized")),
        }

        if self.config.sys_frozen {
            let frozen = b"frozen\0";

            match unsafe {
                pyffi::PySys_SetObject(frozen.as_ptr() as *const c_char, py_true.as_ptr())
            } {
                0 => (),
                _ => return Err(NewInterpreterError::Simple("unable to set sys.frozen")),
            }
        }

        if self.config.sys_meipass {
            let meipass = b"_MEIPASS\0";
            let value = self.config.origin().display().to_string().to_object(py);

            match unsafe {
                pyffi::PySys_SetObject(meipass.as_ptr() as *const c_char, value.as_ptr())
            } {
                0 => (),
                _ => return Err(NewInterpreterError::Simple("unable to set sys._MEIPASS")),
            }
        }

        let write_modules_path = if let Some(key) = &self.config.write_modules_directory_env {
            if let Ok(path) = std::env::var(key) {
                let path = PathBuf::from(path);

                std::fs::create_dir_all(&path).map_err(|e| {
                    NewInterpreterError::Dynamic(format!(
                        "error creating directory for loaded modules files: {}",
                        e
                    ))
                })?;

                // We use Python's uuid module to generate a filename. This avoids
                // a dependency on a Rust crate, which cuts down on dependency bloat.
                let uuid_mod = py.import("uuid").map_err(|e| {
                    NewInterpreterError::new_from_pyerr(py, e, "importing uuid module")
                })?;
                let uuid4 = uuid_mod.getattr("uuid4").map_err(|e| {
                    NewInterpreterError::new_from_pyerr(py, e, "obtaining uuid.uuid4")
                })?;
                let uuid = uuid4.call0().map_err(|e| {
                    NewInterpreterError::new_from_pyerr(py, e, "calling uuid.uuid4()")
                })?;
                let uuid_str = uuid
                    .str()
                    .map_err(|e| {
                        NewInterpreterError::new_from_pyerr(py, e, "converting uuid to str")
                    })?
                    .to_string();

                Some(path.join(format!("modules-{}", uuid_str)))
            } else {
                None
            }
        } else {
            None
        };

        Ok(write_modules_path)
    }

    /// Proxy for [Python::with_gil()].
    ///
    /// This allows running Python code via the PyO3 Rust APIs. Alternatively,
    /// this can be used to run code when the Python GIL is held.
    #[inline]
    pub fn with_gil<F, R>(&self, f: F) -> R
    where
        F: for<'py> FnOnce(Python<'py>) -> R,
    {
        Python::with_gil(f)
    }

    /// Runs `Py_RunMain()` and finalizes the interpreter.
    ///
    /// This will execute whatever is configured by the Python interpreter config
    /// and return an integer suitable for use as a process exit code.
    ///
    /// Calling this function will finalize the interpreter and only gives you an
    /// exit code: there is no opportunity to inspect the return value or handle
    /// an uncaught exception. If you want to keep the interpreter alive or inspect
    /// the evaluation result, consider calling a function on the interpreter handle
    /// that executes code.
    pub fn py_runmain(self) -> i32 {
        unsafe {
            // GIL must be acquired before calling Py_RunMain(). And Py_RunMain()
            // finalizes the interpreter. So we don't need to release the GIL
            // afterwards.
            pyffi::PyGILState_Ensure();
            pyffi::Py_RunMain()
        }
    }

    /// Run in "multiprocessing worker" mode.
    ///
    /// This should be called when `sys.argv[1] == "--multiprocessing-fork"`. It
    /// will parse arguments for the worker from `sys.argv` and call into the
    /// `multiprocessing` module to perform work.
    pub fn run_multiprocessing(&self) -> PyResult<i32> {
        // This code effectively reimplements multiprocessing.spawn.freeze_support(),
        // except entirely in the Rust domain. This function effectively verifies
        // `sys.argv[1] == "--multiprocessing-fork"` then parsed key=value arguments
        // from arguments that follow. The keys are well-defined and guaranteed to
        // be ASCII. The values are either ``None`` or an integer. This enables us
        // to parse the arguments purely from Rust.

        let argv = self.config.resolve_sys_argv().to_vec();

        if argv.len() < 2 {
            panic!("run_multiprocessing() called prematurely; sys.argv does not indicate multiprocessing mode");
        }

        self.with_gil(|py| {
            let kwargs = PyDict::new(py);

            for arg in argv.iter().skip(2) {
                let arg = arg.to_string_lossy();

                let mut parts = arg.splitn(2, '=');

                let key = parts
                    .next()
                    .ok_or_else(|| PyRuntimeError::new_err("invalid multiprocessing argument"))?;
                let value = parts
                    .next()
                    .ok_or_else(|| PyRuntimeError::new_err("invalid multiprocessing argument"))?;

                let value = if value == "None" {
                    py.None()
                } else {
                    let v = value.parse::<isize>().map_err(|e| {
                        PyRuntimeError::new_err(format!(
                            "unable to convert multiprocessing argument to integer: {}",
                            e
                        ))
                    })?;

                    v.into_py(py)
                };

                kwargs.set_item(key, value)?;
            }

            let spawn_module = py.import("multiprocessing.spawn")?;
            spawn_module.getattr("spawn_main")?.call1((kwargs,))?;

            Ok(0)
        })
    }

    /// Whether the Python interpreter is in "multiprocessing worker" mode.
    ///
    /// The `multiprocessing` module can work by spawning new processes
    /// with arguments `--multiprocessing-fork [key=value] ...`. This function
    /// detects if the current Python interpreter is configured for said execution.
    pub fn is_multiprocessing(&self) -> bool {
        let argv = self.config.resolve_sys_argv();

        argv.len() >= 2 && argv[1] == "--multiprocessing-fork"
    }

    /// Runs the Python interpreter.
    ///
    /// If multiprocessing dispatch is enabled, this will check if the
    /// current process invocation appears to be a spawned multiprocessing worker
    /// and dispatch to multiprocessing accordingly.
    ///
    /// Otherwise, this delegates to [Self::py_runmain].
    pub fn run(self) -> i32 {
        if self.config.multiprocessing_auto_dispatch && self.is_multiprocessing() {
            match self.run_multiprocessing() {
                Ok(code) => code,
                Err(e) => {
                    self.with_gil(|py| {
                        e.print(py);
                    });

                    1
                }
            }
        } else {
            self.py_runmain()
        }
    }
}

static mut ORIGINAL_BUILTIN_EXTENSIONS: Option<Vec<pyffi::_inittab>> = None;
static mut REPLACED_BUILTIN_EXTENSIONS: Option<Vec<pyffi::_inittab>> = None;

/// Set PyImport_Inittab from config options.
///
/// CPython has buggy code around memory handling for PyImport_Inittab.
/// See https://github.com/python/cpython/pull/19746. So, we can't trust
/// the official APIs to do the correct thing if there are multiple
/// interpreters per process.
///
/// We maintain our own shadow copy of this array and synchronize it
/// to PyImport_Inittab during interpreter initialization so we don't
/// call the broken APIs.
fn set_pyimport_inittab(config: &OxidizedPythonInterpreterConfig) {
    // If this is our first time, copy the canonical source to our shadow
    // copy.
    unsafe {
        if ORIGINAL_BUILTIN_EXTENSIONS.is_none() {
            let mut entries: Vec<pyffi::_inittab> = Vec::new();

            for i in 0.. {
                let record = pyffi::PyImport_Inittab.offset(i);

                if (*record).name.is_null() {
                    break;
                }

                entries.push(*record);
            }

            ORIGINAL_BUILTIN_EXTENSIONS = Some(entries);
        }
    }

    // Now make a copy and add in new extensions.
    let mut extensions = unsafe { ORIGINAL_BUILTIN_EXTENSIONS.as_ref().unwrap().clone() };

    if config.oxidized_importer {
        let ptr = PyInit_oxidized_importer as *const ();
        extensions.push(pyffi::_inittab {
            name: OXIDIZED_IMPORTER_NAME.as_ptr() as *mut _,
            initfunc: Some(unsafe {
                std::mem::transmute::<*const (), extern "C" fn() -> *mut pyffi::PyObject>(ptr)
            }),
        });
    }

    // Add additional extension modules from the config.
    if let Some(extra_extension_modules) = &config.extra_extension_modules {
        for extension in extra_extension_modules {
            let ptr = extension.init_func as *const ();
            extensions.push(pyffi::_inittab {
                name: extension.name.as_ptr() as *mut _,
                initfunc: Some(unsafe {
                    std::mem::transmute::<*const (), extern "C" fn() -> *mut pyffi::PyObject>(ptr)
                }),
            });
        }
    }

    // Add sentinel record with NULLs.
    extensions.push(pyffi::_inittab {
        name: std::ptr::null_mut(),
        initfunc: None,
    });

    // And finally replace the static in Python's code with our instance.
    unsafe {
        REPLACED_BUILTIN_EXTENSIONS = Some(extensions);
        pyffi::PyImport_Inittab = REPLACED_BUILTIN_EXTENSIONS.as_mut().unwrap().as_mut_ptr();
    }
}

/// Write loaded Python modules to a directory.
///
/// Given a Python interpreter and a path to a directory, this will create a
/// file in that directory named ``modules-<UUID>`` and write a ``\n`` delimited
/// list of loaded names from ``sys.modules`` into that file.
fn write_modules_to_path(py: Python, path: &Path) -> Result<(), &'static str> {
    // TODO this needs better error handling all over.

    let sys = py
        .import("sys")
        .map_err(|_| "could not obtain sys module")?;
    let modules = sys
        .getattr("modules")
        .map_err(|_| "could not obtain sys.modules")?;

    let modules = modules
        .cast_as::<PyDict>()
        .map_err(|_| "sys.modules is not a dict")?;

    let mut names = BTreeSet::new();
    for (key, _value) in modules.iter() {
        names.insert(
            key.extract::<String>()
                .map_err(|_| "module name is not a str")?,
        );
    }

    let mut f = fs::File::create(path).map_err(|_| "could not open file for writing")?;

    for name in names {
        f.write_fmt(format_args!("{}\n", name))
            .map_err(|_| "could not write")?;
    }

    Ok(())
}

impl<'interpreter, 'resources> Drop for MainPythonInterpreter<'interpreter, 'resources> {
    fn drop(&mut self) {
        // Interpreter may have been finalized already. Possibly through our invocation
        // of Py_RunMain(). Possibly something out-of-band beyond our control. We don't
        // muck with the interpreter after finalization because this will likely result
        // in a segfault.
        if unsafe { pyffi::Py_IsInitialized() } == 0 {
            return;
        }

        if let Some(path) = self.write_modules_path.as_ref() {
            match self.with_gil(|py| write_modules_to_path(py, path)) {
                Ok(_) => {}
                Err(msg) => {
                    eprintln!("error writing modules file: {}", msg);
                }
            }
        }

        unsafe {
            pyffi::PyGILState_Ensure();
            pyffi::Py_FinalizeEx();
        }
    }
}