Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

cmake_minimum_required(VERSION 3.15)

project(dart_bridge VERSION 1.2.2 LANGUAGES C)
project(dart_bridge VERSION 1.3.0 LANGUAGES C)

if(NOT DEFINED DART_BRIDGE_PYTHON_INCLUDE_DIRS)
find_package(Python3 REQUIRED COMPONENTS Development.Module)
Expand Down
277 changes: 276 additions & 1 deletion src/dart_bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,17 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "dart_api/dart_api_dl.h"

#if defined(_WIN32)
#define EXPORT __declspec(dllexport)
#elif defined(__ANDROID__)
#include <android/log.h>
#define EXPORT __attribute__((visibility("default")))
#elif defined(__APPLE__)
#include <os/log.h>
#define EXPORT __attribute__((visibility("default")))
#else
#define EXPORT __attribute__((visibility("default")))
#endif
Expand All @@ -56,6 +63,11 @@ typedef struct handler_entry {
// Mutated only while holding the GIL.
static handler_entry* g_handlers = NULL;

// Forward declaration — definition lives further down with the
// session-restart machinery. Called by dart_bridge_clear_handlers around
// Py_Finalize to release session-restart subscribers too.
static void clear_restart_handlers_locked(void);

// Look up a handler by port. Returns borrowed reference (caller is under GIL).
static PyObject* find_handler_locked(int64_t port) {
for (handler_entry* e = g_handlers; e; e = e->next) {
Expand Down Expand Up @@ -95,7 +107,8 @@ static int set_handler_locked(int64_t port, PyObject* handler) {
}

// Called by serious_python_run.c around Py_Finalize so handler PyObjects
// are released before the interpreter goes away.
// (both the port→callable map AND the session-restart subscribers) are
// released before the interpreter goes away.
EXPORT void dart_bridge_clear_handlers(void) {
handler_entry* e = g_handlers;
g_handlers = NULL;
Expand All @@ -105,6 +118,171 @@ EXPORT void dart_bridge_clear_handlers(void) {
free(e);
e = next;
}
clear_restart_handlers_locked();
}

// ---------------------------------------------------------------------------
// Session-restart handler list (Python callbacks fired on Dart VM restart)
//
// On platforms where the OS keeps the process alive across a Dart VM
// restart (notably Android — back-button quit, then re-launch when the OS
// hasn't OOM-killed the process), the new Dart VM allocates fresh
// PythonBridge native ports. The Python program is still running with
// handlers registered on the OLD (now-dead) ports.
//
// `dart_bridge_signal_dart_session` is called by the new Dart VM from
// PythonBridge's first init each launch. If Python is up, it dispatches
// to every registered Python callback with a {label: new_port} dict so
// Python-side consumers (flet's FletDartBridgeServer, the python.dart
// `sys.exit` patcher, etc.) can rewire to the new ports.
// ---------------------------------------------------------------------------

typedef struct restart_entry {
PyObject* handler; // strong ref
struct restart_entry* next;
} restart_entry;

static restart_entry* g_restart_handlers = NULL;

// Add a callback. Steals the reference. GIL held.
static int add_restart_handler_locked(PyObject* handler) {
restart_entry* e = (restart_entry*)malloc(sizeof(restart_entry));
if (!e) {
Py_DECREF(handler);
PyErr_NoMemory();
return -1;
}
e->handler = handler;
e->next = g_restart_handlers;
g_restart_handlers = e;
return 0;
}

// Released alongside g_handlers around Py_Finalize.
static void clear_restart_handlers_locked(void) {
restart_entry* e = g_restart_handlers;
g_restart_handlers = NULL;
while (e) {
restart_entry* next = e->next;
Py_XDECREF(e->handler);
free(e);
e = next;
}
}

// Reports whether the embedded interpreter is up. Lets the Dart side
// detect process-reuse on its second startup.
EXPORT int dart_bridge_is_python_initialized(void) {
return Py_IsInitialized() ? 1 : 0;
}

// Build a {label: port} Python dict from parallel C arrays. Returns a new
// reference; NULL on error (PyErr set). Caller holds the GIL.
static PyObject* build_port_map(int n_pairs,
const char* const* labels,
const int64_t* ports) {
PyObject* d = PyDict_New();
if (!d) return NULL;
for (int i = 0; i < n_pairs; i++) {
if (!labels[i]) continue;
PyObject* v = PyLong_FromLongLong((long long)ports[i]);
if (!v) { Py_DECREF(d); return NULL; }
if (PyDict_SetItemString(d, labels[i], v) != 0) {
Py_DECREF(v); Py_DECREF(d); return NULL;
}
Py_DECREF(v);
}
return d;
}

// Called by Dart on EVERY VM startup with the labeled new port numbers.
// If Python isn't up yet → no-op (fresh-start path uses env vars).
// If Python IS up → dispatch to every registered handler with the port
// map. Errors raised by handlers are printed (don't abort the others).
//
// Parallel arrays kept for FFI simplicity: labels[i] / ports[i] form one
// (key, value) pair. n_pairs is the count.
EXPORT void dart_bridge_signal_dart_session(int n_pairs,
const char* const* labels,
const int64_t* ports) {
if (!Py_IsInitialized()) {
return;
}
PyGILState_STATE gstate = PyGILState_Ensure();

PyObject* port_map = build_port_map(n_pairs, labels, ports);
if (!port_map) {
PyErr_Print();
PyGILState_Release(gstate);
return;
}

for (restart_entry* e = g_restart_handlers; e; e = e->next) {
PyObject* result = PyObject_CallFunctionObjArgs(e->handler, port_map, NULL);
if (!result) {
PyErr_Print(); // don't abort remaining handlers
}
Py_XDECREF(result);
}

Py_DECREF(port_map);
PyGILState_Release(gstate);
}

// ---------------------------------------------------------------------------
// stdout/stderr redirection
//
// On Android and iOS, fd 1/2 from a Flutter app go nowhere by default —
// Python `print()` and tracebacks become invisible. After Py_Initialize
// we replace sys.stdout / sys.stderr with file-like wrappers whose
// `write()` calls native_log_write, which dispatches to logcat / os_log /
// fwrite per platform.
// ---------------------------------------------------------------------------

static void native_log_write(int is_stderr, const char* buf, Py_ssize_t len) {
if (len <= 0) return;
#if defined(__ANDROID__)
// __android_log_write expects a NUL-terminated string. Buffer is
// typically short (one write call per print line); copy + NUL-pad.
char stack_buf[1024];
char* tmp = stack_buf;
if ((size_t)len + 1 > sizeof(stack_buf)) {
tmp = (char*)malloc((size_t)len + 1);
if (!tmp) return;
}
memcpy(tmp, buf, (size_t)len);
tmp[len] = '\0';
// Strip a single trailing newline so logcat doesn't double-space —
// each __android_log_write call already adds its own line break.
if (len > 0 && tmp[len - 1] == '\n') tmp[len - 1] = '\0';
__android_log_write(is_stderr ? ANDROID_LOG_ERROR : ANDROID_LOG_INFO,
"flet.python", tmp);
if (tmp != stack_buf) free(tmp);
#elif defined(__APPLE__)
// os_log truncates at ~1KB by default; for longer payloads we'd want
// to chunk. Most print()s are well under that. is_stderr maps to
// OS_LOG_TYPE_ERROR, otherwise OS_LOG_TYPE_DEFAULT.
char stack_buf[1024];
char* tmp = stack_buf;
if ((size_t)len + 1 > sizeof(stack_buf)) {
tmp = (char*)malloc((size_t)len + 1);
if (!tmp) return;
}
memcpy(tmp, buf, (size_t)len);
tmp[len] = '\0';
if (len > 0 && tmp[len - 1] == '\n') tmp[len - 1] = '\0';
if (is_stderr) {
os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_ERROR, "%{public}s", tmp);
} else {
os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_DEFAULT, "%{public}s", tmp);
}
if (tmp != stack_buf) free(tmp);
#else
// Desktop: fd 1/2 work — passthrough preserves existing behavior so
// `flet run` console output, CI logs, etc. keep flowing.
fwrite(buf, 1, (size_t)len, is_stderr ? stderr : stdout);
if (is_stderr) fflush(stderr);
#endif
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -215,6 +393,30 @@ static PyObject* py_send_bytes(PyObject* self, PyObject* args) {
Py_RETURN_TRUE;
}

static PyObject* py_add_session_restart_handler(PyObject* self, PyObject* arg) {
if (!PyCallable_Check(arg)) {
PyErr_SetString(PyExc_TypeError, "argument must be callable");
return NULL;
}
Py_INCREF(arg);
if (add_restart_handler_locked(arg) != 0) return NULL; // steals our ref
Py_RETURN_NONE;
}

// Internal: invoked from the Python file-like wrappers installed as
// sys.stdout / sys.stderr. Two args: (is_stderr: int, text: str). No
// return value (returns None).
static PyObject* py_native_log_write(PyObject* self, PyObject* args) {
int is_stderr;
const char* text;
Py_ssize_t length;
if (!PyArg_ParseTuple(args, "is#:_native_log_write", &is_stderr, &text, &length)) {
return NULL;
}
native_log_write(is_stderr, text, length);
Py_RETURN_NONE;
}

static PyMethodDef dart_bridge_methods[] = {
{"send_bytes", py_send_bytes, METH_VARARGS,
"send_bytes(port, payload) — post a bytes payload to the Dart\n"
Expand All @@ -223,6 +425,16 @@ static PyMethodDef dart_bridge_methods[] = {
"set_enqueue_handler_func(port, callable) — register a Python\n"
"callable that receives bytes posted from Dart for the given port.\n"
"Pass None as the callable to unregister."},
{"add_session_restart_handler", py_add_session_restart_handler, METH_O,
"add_session_restart_handler(callable) — register a callback that\n"
"fires when a new Dart VM signals the running Python program with\n"
"fresh native port numbers (e.g. Android process reuse). The callback\n"
"receives a {label: port} dict. Multiple subscribers supported; each\n"
"gets the full map."},
{"_native_log_write", py_native_log_write, METH_VARARGS,
"_native_log_write(is_stderr, text) — internal. Forwards bytes to the\n"
"platform's native log sink (logcat / os_log / stderr). Used by the\n"
"sys.stdout / sys.stderr wrappers installed at interpreter start."},
{NULL, NULL, 0, NULL}
};

Expand All @@ -236,3 +448,66 @@ static struct PyModuleDef dart_bridge_module = {
PyMODINIT_FUNC PyInit_dart_bridge(void) {
return PyModule_Create(&dart_bridge_module);
}

// Install Python-level sys.stdout / sys.stderr wrappers that forward
// writes to native_log_write. Called by serious_python_run.c right after
// Py_Initialize so even early bootstrap prints land in logcat / os_log.
//
// We do this in Python (not by replacing fd 1/2 directly) because
// Python's stdio is layered: PyObject_Print → sys.stdout.write → fd. The
// fd-replacement approach (dup2 onto a pipe + a reader thread) works but
// adds a thread + buffering; intercepting at sys.stdout is simpler and
// catches `print` directly. Native crashes that write directly to fd
// won't surface here — those need a different mechanism if we ever care.
EXPORT int dart_bridge_install_stdio_redirect(void) {
if (!Py_IsInitialized()) return -1;
PyGILState_STATE gstate = PyGILState_Ensure();

// Define a tiny file-like class in Python that calls back into
// dart_bridge._native_log_write. Keep the implementation in Python
// (rather than building a PyType_Spec by hand) — Limited API safe and
// far less code. The class is anonymous (lives in __main__).
static const char* installer_src =
"import sys, dart_bridge\n"
"class _DartBridgeNativeWriter:\n"
" def __init__(self, is_stderr):\n"
" self._is_stderr = 1 if is_stderr else 0\n"
" def write(self, text):\n"
" if text:\n"
" dart_bridge._native_log_write(self._is_stderr, str(text))\n"
" return len(text) if text else 0\n"
" def flush(self):\n"
" pass\n"
" def isatty(self):\n"
" return False\n"
" def fileno(self):\n"
" raise OSError('no fileno on native log writer')\n"
"sys.stdout = _DartBridgeNativeWriter(False)\n"
"sys.stderr = _DartBridgeNativeWriter(True)\n";

PyObject* code = Py_CompileString(installer_src, "<dart_bridge_stdio>",
Py_file_input);
if (!code) {
PyErr_Print();
PyGILState_Release(gstate);
return -1;
}
PyObject* main_mod = PyImport_AddModule("__main__"); // borrowed
if (!main_mod) {
Py_DECREF(code);
PyErr_Print();
PyGILState_Release(gstate);
return -1;
}
PyObject* globals = PyModule_GetDict(main_mod); // borrowed
PyObject* result = PyEval_EvalCode(code, globals, globals);
Py_DECREF(code);
if (!result) {
PyErr_Print();
PyGILState_Release(gstate);
return -1;
}
Py_DECREF(result);
PyGILState_Release(gstate);
return 0;
}
Loading