Skip to content

Commit 04cda82

Browse files
committed
Use long-lived isolate for Python execution
Introduces a single, persistent isolate to run Python code, ensuring the 'main thread' identity is preserved across invocations. Adds signal handling patch for non-main threads to prevent errors with libraries like asyncio. Refactors run logic to serialize all Python runs on one thread for consistency.
1 parent 2347501 commit 04cda82

File tree

1 file changed

+54
-14
lines changed

1 file changed

+54
-14
lines changed

src/serious_python_android/lib/src/cpython.dart

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import 'gen.dart';
1111
export 'gen.dart';
1212

1313
CPython? _cpython;
14+
// Keep a single, long-lived isolate/thread for running Python to preserve the
15+
// "main thread" identity across invocations.
16+
Isolate? _pythonIsolate;
17+
SendPort? _pythonSendPort;
18+
bool _signalPatched = false;
1419

1520
void _pyLog(String msg) {
1621
debugPrint("[PY] ${DateTime.now().toIso8601String()} $msg");
@@ -22,23 +27,45 @@ CPython getCPython(String dynamicLibPath) {
2227

2328
Future<String> runPythonProgramFFI(bool sync, String dynamicLibPath,
2429
String pythonProgramPath, String script) async {
25-
final receivePort = ReceivePort();
2630
if (sync) {
27-
// sync run
31+
// sync run on current isolate/thread
32+
final rp = ReceivePort();
2833
return await runPythonProgramInIsolate(
29-
[receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]);
30-
} else {
31-
var completer = Completer<String>();
32-
// async run
33-
final isolate = await Isolate.spawn(runPythonProgramInIsolate,
34-
[receivePort.sendPort, dynamicLibPath, pythonProgramPath, script]);
35-
receivePort.listen((message) {
36-
receivePort.close();
37-
isolate.kill();
38-
completer.complete(message);
39-
});
40-
return completer.future;
34+
[rp.sendPort, dynamicLibPath, pythonProgramPath, script]);
35+
}
36+
37+
// Ensure a single long-lived isolate/thread to keep Python "main thread" consistent.
38+
if (_pythonIsolate == null || _pythonSendPort == null) {
39+
final readyPort = ReceivePort();
40+
_pythonIsolate =
41+
await Isolate.spawn(_pythonIsolateMain, readyPort.sendPort);
42+
_pythonSendPort = await readyPort.first as SendPort;
43+
readyPort.close();
4144
}
45+
46+
final response = ReceivePort();
47+
_pythonSendPort!
48+
.send([response.sendPort, dynamicLibPath, pythonProgramPath, script]);
49+
final result = await response.first as String;
50+
response.close();
51+
return result;
52+
}
53+
54+
// Long-lived isolate entry point to serialize all Python runs on one thread.
55+
void _pythonIsolateMain(SendPort readyPort) {
56+
final commandPort = ReceivePort();
57+
readyPort.send(commandPort.sendPort);
58+
commandPort.listen((message) async {
59+
final args = message as List<Object>;
60+
final sendPort = args[0] as SendPort;
61+
final dynamicLibPath = args[1] as String;
62+
final pythonProgramPath = args[2] as String;
63+
final script = args[3] as String;
64+
final result = await runPythonProgramInIsolate(
65+
[sendPort, dynamicLibPath, pythonProgramPath, script]);
66+
// runPythonProgramInIsolate already sends the result, but return anyway.
67+
sendPort.send(result);
68+
});
4269
}
4370

4471
Future<String> runPythonProgramInIsolate(List<Object> arguments) async {
@@ -72,6 +99,19 @@ Future<String> runPythonProgramInIsolate(List<Object> arguments) async {
7299
final gilState = cpython.PyGILState_Ensure();
73100
_pyLog("PyGILState_Ensure -> $gilState");
74101

102+
// Patch signal handling if we're not on the interpreter's main thread; some
103+
// libraries (e.g., asyncio) call signal.signal which would otherwise raise.
104+
if (!_signalPatched) {
105+
const patch = """
106+
import threading, signal
107+
if threading.current_thread() is not threading.main_thread():
108+
signal.signal = lambda *args, **kwargs: None
109+
""";
110+
_pyLog("patching signal.signal for non-main thread use");
111+
cpython.PyRun_SimpleString(patch.toNativeUtf8().cast());
112+
_signalPatched = true;
113+
}
114+
75115
try {
76116
if (script != "") {
77117
// run script

0 commit comments

Comments
 (0)