Skip to content

Commit 1b86e28

Browse files
committed
Shim split: dart_bridge.c (Dart-callable) + dart_bridge_shim.c (Python module)
Fixes the Linux/Windows/Android bridge_example handshake-timeout failure by making Python and Dart share the SAME `global_enqueue_handler_func` cell on every platform. Before: the wheel-installed dart_bridge.so (cibuildwheel) and the Flutter-built libflet_bridge.so each compiled a full copy of dart_bridge.c — two separate binary instances, two separate copies of the static `global_enqueue_handler_func`. Python's set_enqueue_handler_func wrote to the wheel's copy; Dart's DartBridge_EnqueueMessage read libflet_bridge's copy (NULL); messages were silently dropped. After: the core lives in libflet_bridge ONLY. The wheel ships a thin shim that resolves the core's exports at PyInit time via runtime symbol lookup (dlsym(RTLD_DEFAULT) → dlopen RTLD_GLOBAL fallback on Linux/macOS, LoadLibrary+GetProcAddress on Windows). One global cell, visible to both sides. Changes: - native/dart_bridge.c: drop the Python-callable methods. Exports three symbols for the shim to resolve: * dart_bridge_global_enqueue_handler_func (PyObject*, was static) * dart_bridge_post_to_dart (helper that wraps Dart_PostCObject_DL so the shim doesn't need its own copy of dart_api_dl.c) * existing DartBridge_InitDartApiDL + DartBridge_EnqueueMessage - native/dart_bridge_shim.c (NEW): PyInit_dart_bridge resolves the three exports above via shim_sym_lookup(); set_enqueue_handler_func writes through the resolved pointer; send_bytes calls through the resolved function pointer. ImportError if libflet_bridge isn't loaded into the process (catches misconfiguration loudly). - python/setup.py: compile only dart_bridge_shim.c; add -ldl on Linux for the dlopen/dlsym used by the shim. Dropped dart_api_dl.c from the wheel's sources (the shim doesn't call Dart_PostCObject_DL directly). - darwin/Classes/dart_bridge_shim.c (symlink): so the Apple static link picks both .c files into the framework. Apple's dlsym(RTLD_DEFAULT) finds the symbols in-process (smoke-tested: macOS bridge_example_macos still passes). - ci.yml + test-bridge-build.yml: drop continue-on-error from bridge_example_linux and bridge_example_windows — they should pass now. Verified locally on macOS: `flutter test integration_test -d macos` still green. CI verification pending — pushing to dart-bridge will exercise all three desktop platforms.
1 parent e3aa566 commit 1b86e28

6 files changed

Lines changed: 171 additions & 89 deletions

File tree

.github/workflows/ci.yml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,6 @@ jobs:
315315
# Python (no static-link path like on Apple); pull it from the wheel build.
316316
needs: release_build
317317
if: startsWith(github.ref, 'refs/tags/v')
318-
# KNOWN DEFERRED: symbol visibility between the wheel's dart_bridge.so and
319-
# the Flutter-built libflet_bridge.so means Python's enqueue handler is
320-
# never seen by Dart's DartBridge_EnqueueMessage (each has its own copy
321-
# of global_enqueue_handler_func). Proper fix is the shim split from the
322-
# original plan — tracked in bridge-followups.md. continue-on-error so
323-
# the test doesn't block tag releases until the split lands.
324-
continue-on-error: true
325318
strategy:
326319
fail-fast: false
327320
matrix:
@@ -406,9 +399,6 @@ jobs:
406399
runs-on: windows-latest
407400
needs: release_build
408401
if: startsWith(github.ref, 'refs/tags/v')
409-
# Same KNOWN DEFERRED as bridge_example_linux above — symbol visibility
410-
# between wheel and libflet_bridge.dll. Tracked in bridge-followups.md.
411-
continue-on-error: true
412402
strategy:
413403
fail-fast: false
414404
matrix:

.github/workflows/test-bridge-build.yml

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -189,16 +189,6 @@ jobs:
189189
name: Bridge example Linux ${{ matrix.title }} round-trip (Python ${{ matrix.python_version }})
190190
runs-on: ${{ matrix.runner }}
191191
needs: test_wheel_build
192-
# KNOWN DEFERRED: the wheel-installed dart_bridge.so and the Flutter-built
193-
# libflet_bridge.so are separate binaries with separate copies of
194-
# global_enqueue_handler_func. Python's set_enqueue_handler_func writes
195-
# to the wheel's copy; Dart's DartBridge_EnqueueMessage reads
196-
# libflet_bridge.so's copy, which is always NULL. Test currently times
197-
# out at handshake echo. Proper fix is the shim split from the original
198-
# plan (dart_bridge_shim.c that dlopens libflet_bridge.so RTLD_GLOBAL) —
199-
# tracked in bridge-followups.md. continue-on-error so this matrix
200-
# leaves data without blocking the rest of CI.
201-
continue-on-error: true
202192
strategy:
203193
fail-fast: false
204194
matrix:
@@ -279,10 +269,6 @@ jobs:
279269
name: Bridge example Windows round-trip (Python ${{ matrix.python_version }})
280270
runs-on: windows-latest
281271
needs: test_wheel_build
282-
# Same KNOWN DEFERRED as test_bridge_example_linux above — symbol
283-
# visibility between wheel and libflet_bridge.dll. Tracked in
284-
# bridge-followups.md.
285-
continue-on-error: true
286272
strategy:
287273
fail-fast: false
288274
matrix:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../native/dart_bridge_shim.c

src/serious_python_bridge/native/dart_bridge.c

Lines changed: 25 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,21 @@
1515
#endif
1616

1717
// ---------------------------------------------------------------------------
18-
// Core: symbols called from Dart via FFI. On platforms with the shared-lib
19-
// split these live in libflet_bridge; on iOS they're linked statically into
20-
// the serious_python framework.
18+
// Core: symbols called from Dart via FFI plus exported helpers the Python-side
19+
// shim (dart_bridge_shim.c) resolves at runtime via dlsym. Compiled into
20+
// libflet_bridge.{so,dll,dylib} by the Flutter plugin build. On Apple
21+
// platforms also compiled into the static archive linked into the
22+
// serious_python framework alongside dart_bridge_shim.c.
23+
//
24+
// The shim NEVER defines its own copy of these symbols — it always looks them
25+
// up at runtime. That keeps Dart's view of `global_enqueue_handler_func` and
26+
// the Python shim's view as a single shared cell on every platform.
2127
// ---------------------------------------------------------------------------
2228

23-
static PyObject* global_enqueue_handler_func = NULL;
29+
// Exported (non-static) so dart_bridge_shim.c's set_enqueue_handler_func can
30+
// write to it via dlsym. Initialised to NULL; the shim swaps in a PyObject*
31+
// callable when Python registers a handler.
32+
EXPORT PyObject* dart_bridge_global_enqueue_handler_func = NULL;
2433

2534
EXPORT intptr_t DartBridge_InitDartApiDL(void* data) {
2635
return Dart_InitializeApiDL(data);
@@ -37,7 +46,7 @@ EXPORT void DartBridge_EnqueueMessage(const char* data, size_t len) {
3746

3847
PyGILState_STATE gstate = PyGILState_Ensure();
3948

40-
if (!global_enqueue_handler_func) {
49+
if (!dart_bridge_global_enqueue_handler_func) {
4150
fprintf(stderr, "[dart_bridge] enqueue handler is not registered\n");
4251
PyGILState_Release(gstate);
4352
return;
@@ -50,7 +59,8 @@ EXPORT void DartBridge_EnqueueMessage(const char* data, size_t len) {
5059
return;
5160
}
5261

53-
PyObject* result = PyObject_CallFunctionObjArgs(global_enqueue_handler_func, arg, NULL);
62+
PyObject* result = PyObject_CallFunctionObjArgs(
63+
dart_bridge_global_enqueue_handler_func, arg, NULL);
5464
if (!result) {
5565
PyErr_Print();
5666
}
@@ -60,49 +70,22 @@ EXPORT void DartBridge_EnqueueMessage(const char* data, size_t len) {
6070
PyGILState_Release(gstate);
6171
}
6272

63-
// ---------------------------------------------------------------------------
64-
// Shim: Python-callable methods exposed by the `dart_bridge` module.
65-
// ---------------------------------------------------------------------------
66-
67-
static PyObject* set_enqueue_handler_func(PyObject* self, PyObject* args) {
68-
PyObject* func;
69-
70-
if (!PyArg_ParseTuple(args, "O:set_enqueue_handler_func", &func)) {
71-
return NULL;
72-
}
73-
74-
if (!PyCallable_Check(func)) {
75-
PyErr_SetString(PyExc_TypeError, "parameter must be callable");
76-
return NULL;
77-
}
78-
79-
Py_XINCREF(func);
80-
Py_XDECREF(global_enqueue_handler_func);
81-
global_enqueue_handler_func = func;
82-
83-
Py_RETURN_NONE;
84-
}
85-
86-
static PyObject* send_bytes(PyObject* self, PyObject* args) {
87-
int64_t port;
88-
const char* buffer;
89-
Py_ssize_t length;
90-
91-
if (!PyArg_ParseTuple(args, "Ly#", &port, &buffer, &length)) {
92-
return NULL;
93-
}
94-
73+
// Exported helper called by the shim's send_bytes(). Keeps the
74+
// Dart_PostCObject_DL invocation in this translation unit so the shim doesn't
75+
// need its own copy of dart_api_dl.c. Returns 0 on success, -1 on failure with
76+
// a Python exception set.
77+
EXPORT int dart_bridge_post_to_dart(int64_t port, const char* buffer, size_t length) {
9578
if (port == 0) {
9679
PyErr_SetString(PyExc_RuntimeError, "Dart port is 0 (invalid)");
97-
return NULL;
80+
return -1;
9881
}
9982

10083
// Dart_PostCObject_DL is a function pointer populated by Dart_InitializeApiDL.
10184
// Calling it before init segfaults; surface a clean error instead.
10285
if (Dart_PostCObject_DL == NULL) {
10386
PyErr_SetString(PyExc_RuntimeError,
10487
"Dart API DL not initialized (call DartBridge_InitDartApiDL from Dart first)");
105-
return NULL;
88+
return -1;
10689
}
10790

10891
Dart_CObject obj;
@@ -113,24 +96,7 @@ static PyObject* send_bytes(PyObject* self, PyObject* args) {
11396

11497
if (!Dart_PostCObject_DL(port, &obj)) {
11598
PyErr_SetString(PyExc_RuntimeError, "Dart_PostCObject_DL failed");
116-
return NULL;
99+
return -1;
117100
}
118-
119-
Py_RETURN_TRUE;
120-
}
121-
122-
static PyMethodDef methods[] = {
123-
{"send_bytes", send_bytes, METH_VARARGS, "Post a bytes payload to a Dart ReceivePort."},
124-
{"set_enqueue_handler_func", set_enqueue_handler_func, METH_VARARGS,
125-
"Register the Python callable that receives bytes posted from Dart."},
126-
{NULL, NULL, 0, NULL}
127-
};
128-
129-
static struct PyModuleDef moduledef = {
130-
PyModuleDef_HEAD_INIT,
131-
"dart_bridge", NULL, -1, methods
132-
};
133-
134-
PyMODINIT_FUNC PyInit_dart_bridge(void) {
135-
return PyModule_Create(&moduledef);
101+
return 0;
136102
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Python-callable shim for the dart_bridge module.
2+
//
3+
// This file is the *only* source compiled into the dart_bridge wheel built by
4+
// cibuildwheel. It contains no Dart-callable symbols and no copy of the
5+
// shared `dart_bridge_global_enqueue_handler_func` cell — instead it resolves
6+
// the core's exports (defined in dart_bridge.c, linked into libflet_bridge)
7+
// at PyInit time via dlsym/GetProcAddress. That keeps Dart's view and
8+
// Python's view of the global as the SAME cell on Linux/Windows/Android.
9+
//
10+
// On Apple platforms this same file is also static-linked into the
11+
// serious_python framework alongside dart_bridge.c — the runtime lookup
12+
// then resolves to the symbols statically linked into the host binary
13+
// (dlopen of libflet_bridge is skipped because there's no such file).
14+
15+
#define PY_SSIZE_T_CLEAN
16+
#define Py_LIMITED_API 0x030c0000
17+
#include <Python.h>
18+
#include <stdint.h>
19+
#include <stdio.h>
20+
21+
#if defined(_WIN32)
22+
#include <windows.h>
23+
#else
24+
#include <dlfcn.h>
25+
#endif
26+
27+
// Function-pointer + global types resolved at PyInit time.
28+
typedef int (*PostToDartFn)(int64_t port, const char* buffer, size_t length);
29+
30+
static PyObject** g_handler_slot = NULL; // points at dart_bridge_global_enqueue_handler_func in libflet_bridge
31+
static PostToDartFn g_post_to_dart = NULL; // dart_bridge_post_to_dart in libflet_bridge
32+
33+
#if defined(_WIN32)
34+
static void* shim_sym_lookup(const char* name) {
35+
// LoadLibraryA returns the existing handle if flet_bridge.dll is already
36+
// loaded by Flutter (single instance, same memory, just bumps refcount).
37+
// The DLL search path includes the executable's directory, where Flutter
38+
// places plugin DLLs.
39+
HMODULE flet = LoadLibraryA("flet_bridge.dll");
40+
if (!flet) return NULL;
41+
return (void*)GetProcAddress(flet, name);
42+
}
43+
#else
44+
static void* shim_sym_lookup(const char* name) {
45+
// RTLD_DEFAULT searches every library already loaded into the process
46+
// including ones loaded with RTLD_LOCAL by Dart's DynamicLibrary.open.
47+
void* p = dlsym(RTLD_DEFAULT, name);
48+
if (p) return p;
49+
// Not visible globally (Dart loaded libflet_bridge with RTLD_LOCAL). Try
50+
// an explicit RTLD_GLOBAL dlopen so subsequent lookups see it. dlopen of
51+
// an already-loaded library returns the existing handle — single instance,
52+
// same memory, just promoted into the global namespace.
53+
#if defined(__APPLE__)
54+
void* h = dlopen("libflet_bridge.dylib", RTLD_NOW | RTLD_GLOBAL);
55+
#else
56+
void* h = dlopen("libflet_bridge.so", RTLD_NOW | RTLD_GLOBAL);
57+
#endif
58+
if (!h) return NULL;
59+
return dlsym(h, name);
60+
}
61+
#endif
62+
63+
static PyObject* set_enqueue_handler_func(PyObject* self, PyObject* args) {
64+
PyObject* func;
65+
66+
if (!PyArg_ParseTuple(args, "O:set_enqueue_handler_func", &func)) {
67+
return NULL;
68+
}
69+
if (!PyCallable_Check(func)) {
70+
PyErr_SetString(PyExc_TypeError, "parameter must be callable");
71+
return NULL;
72+
}
73+
if (!g_handler_slot) {
74+
PyErr_SetString(PyExc_RuntimeError,
75+
"dart_bridge: libflet_bridge symbol not resolved (was the bridge plugin loaded?)");
76+
return NULL;
77+
}
78+
79+
Py_XINCREF(func);
80+
Py_XDECREF(*g_handler_slot);
81+
*g_handler_slot = func;
82+
83+
Py_RETURN_NONE;
84+
}
85+
86+
static PyObject* send_bytes(PyObject* self, PyObject* args) {
87+
int64_t port;
88+
const char* buffer;
89+
Py_ssize_t length;
90+
91+
if (!PyArg_ParseTuple(args, "Ly#", &port, &buffer, &length)) {
92+
return NULL;
93+
}
94+
if (!g_post_to_dart) {
95+
PyErr_SetString(PyExc_RuntimeError,
96+
"dart_bridge: libflet_bridge symbol not resolved (was the bridge plugin loaded?)");
97+
return NULL;
98+
}
99+
if (g_post_to_dart(port, buffer, (size_t)length) != 0) {
100+
// Helper sets the exception.
101+
return NULL;
102+
}
103+
Py_RETURN_TRUE;
104+
}
105+
106+
static PyMethodDef methods[] = {
107+
{"send_bytes", send_bytes, METH_VARARGS, "Post a bytes payload to a Dart ReceivePort."},
108+
{"set_enqueue_handler_func", set_enqueue_handler_func, METH_VARARGS,
109+
"Register the Python callable that receives bytes posted from Dart."},
110+
{NULL, NULL, 0, NULL}
111+
};
112+
113+
static struct PyModuleDef moduledef = {
114+
PyModuleDef_HEAD_INIT,
115+
"dart_bridge", NULL, -1, methods
116+
};
117+
118+
PyMODINIT_FUNC PyInit_dart_bridge(void) {
119+
// Resolve the libflet_bridge exports we depend on. Surface a clean
120+
// ImportError if the lookup fails — typically means the bridge plugin's
121+
// native library wasn't loaded into the process before Python ran.
122+
g_handler_slot = (PyObject**)shim_sym_lookup("dart_bridge_global_enqueue_handler_func");
123+
g_post_to_dart = (PostToDartFn)shim_sym_lookup("dart_bridge_post_to_dart");
124+
if (!g_handler_slot || !g_post_to_dart) {
125+
PyErr_SetString(PyExc_ImportError,
126+
"dart_bridge: failed to resolve libflet_bridge symbols "
127+
"(is serious_python_bridge's native library loaded into the process?)");
128+
return NULL;
129+
}
130+
return PyModule_Create(&moduledef);
131+
}
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1+
import sys
12
from setuptools import Extension, setup
23

34
NATIVE_ROOT = "../native"
45

6+
extra_link_args = []
7+
# dlopen/dlsym on Linux live in libdl. macOS rolls them into libSystem.
8+
# Windows uses LoadLibrary/GetProcAddress from kernel32 (implicit).
9+
if sys.platform.startswith("linux"):
10+
extra_link_args.append("-ldl")
11+
512
setup(
613
ext_modules=[
714
Extension(
815
"dart_bridge",
9-
sources=[
10-
f"{NATIVE_ROOT}/dart_bridge.c",
11-
f"{NATIVE_ROOT}/dart_api/dart_api_dl.c",
12-
],
13-
include_dirs=[NATIVE_ROOT],
16+
# Shim only — the Dart-callable core and its dart_api_dl.c live in
17+
# libflet_bridge (built by the Flutter plugin) and are resolved at
18+
# PyInit time via dlsym / GetProcAddress.
19+
sources=[f"{NATIVE_ROOT}/dart_bridge_shim.c"],
1420
# Limited API / abi3: one .so per platform works for any Python
15-
# 3.12+. Keep this in lockstep with Py_LIMITED_API in dart_bridge.c.
21+
# 3.12+. Keep this in lockstep with Py_LIMITED_API in
22+
# dart_bridge_shim.c.
1623
define_macros=[("Py_LIMITED_API", "0x030c0000")],
1724
py_limited_api=True,
25+
extra_link_args=extra_link_args,
1826
)
1927
],
2028
)

0 commit comments

Comments
 (0)