feat: 添加 Python→Go 全双工回调支持(call_go)

- 新增 WithHandlers 选项,通过反射将 Go 结构体方法暴露给 Python
- 新增 callback/callback_result 消息类型,支持 Python 在处理中回调 Go
- client 侧新增 readResult,内联处理 callback,复用同一连接避免死锁
- Python 侧新增 call_go[T]() 泛型调用,支持 dataclass 自动构造
- 注入 GOBRIDGE_WORKER_ID/WORKER_COUNT 环境变量,支持多 worker 初始化分工
- 新增示例演示 Go→Python→Go→Python 四层全双工链路
- Python 包版本升至 0.1.1
This commit is contained in:
2026-04-14 13:06:50 +08:00
parent 07e9239ac5
commit b390effd8e
8 changed files with 490 additions and 54 deletions

View File

@@ -3,7 +3,7 @@ gobridge - Python 端库,配合 Go 侧 gobridge 使用
用法::
from gobridge import expose, run
from gobridge import expose, call_go, run
from typing import Iterator
@expose
@@ -27,10 +27,17 @@ gobridge - Python 端库,配合 Go 侧 gobridge 使用
total += i # ctx 取消时这里会抛 InterruptedError
return total
# 全双工:在 handler 内调用 Go 注册的方法(需配合 gobridge.NewServer
@expose
def greet(name: str) -> str:
prefix = call_go("GetPrefix") # 调用 Go 注册的 GetPrefix 方法
return f"{prefix}, {name}!"
run()
"""
import ctypes
import dataclasses
import inspect
import json
import os
@@ -39,9 +46,40 @@ import signal
import socket
import struct
import threading
from typing import Any, Callable, TypeVar
T = TypeVar("T")
_exposed: dict = {}
# ─── Worker 标识 ──────────────────────────────────────────────────────────────
# 每个 worker 进程独有的序号0-based和总数由 Go 启动时通过环境变量注入。
# 用于避免多 worker 场景下的重复初始化(如监听端口、建立长连接等):
#
# if gobridge.worker_id == 0:
# start_websocket_server() # 只有 worker 0 才执行
#
worker_id: int = int(os.environ.get("GOBRIDGE_WORKER_ID", "0"))
worker_count: int = int(os.environ.get("GOBRIDGE_WORKER_COUNT", "1"))
# ─── 线程局部存储 ─────────────────────────────────────────────────────────────
# _local.mux 当前线程正在服务的 _ConnMux由 _dispatch 写入)
_local = threading.local()
# ─── call_go 状态 ─────────────────────────────────────────────────────────────
# _cb_pending: callback id → Queue用于接收 Go 发回的 callback_result
_cb_pending: dict[int, queue.Queue] = {}
_cb_lock = threading.Lock()
_cb_id_counter = 0
_cb_id_lock = threading.Lock()
def _next_cb_id() -> int:
global _cb_id_counter
with _cb_id_lock:
_cb_id_counter += 1
return _cb_id_counter
def expose(fn):
"""装饰器:将函数暴露给 Go 侧调用"""
@@ -49,6 +87,71 @@ def expose(fn):
return fn
def _cast(t: type, value: Any) -> Any:
"""将 Go 返回的 JSON 值转换为指定类型。
- dataclass用 dict 字段构造实例
- 其余类型JSON 已经是正确的 Python 原生类型,直接返回
"""
if value is None or t is type(None):
return value
if dataclasses.is_dataclass(t) and not isinstance(value, t) and isinstance(value, dict):
return t(**value)
return value
class _CallGoType:
"""实现 call_go 和 call_go[Type] 两种调用形式。
用法::
call_go("Multiply", 3, 4) # 返回 Any
call_go[int]("Multiply", 3, 4) # 返回 int类型检查器可感知
call_go[User]("GetUser", uid) # 返回 User dataclass 实例
"""
def __call__(self, method: str, *args) -> Any:
return self._invoke(method, args)
def __getitem__(self, t: type[T]) -> Callable[..., T]:
def _typed(method: str, *args) -> T:
return _cast(t, self._invoke(method, args))
return _typed
def _invoke(self, method: str, args: tuple) -> Any:
"""在 Python handler 内调用 Go 注册的方法(全双工回调)。
通过**同一条连接**发送 callback 消息并同步等待 Go 的回复,
保证整条调用链 Go→Python→Go→... 始终串行执行、不产生额外线程。
只能在 gobridge handler通过 @expose 注册、由 gobridge.NewServer 调用)内使用。
在流式输入 handler 中,应在迭代器耗尽后才调用(流式写入期间 Go 正在发送 chunk
"""
mux: "_ConnMux | None" = getattr(_local, "mux", None)
if mux is None:
raise RuntimeError("call_go() must be called within a gobridge handler")
cb_id = _next_cb_id()
result_q: queue.Queue = queue.Queue(1)
with _cb_lock:
_cb_pending[cb_id] = result_q
try:
mux.write({"id": cb_id, "type": "callback", "method": method, "args": list(args)})
# 阻塞等待 Go 回复ctx 取消时 _raise_in_thread 会注入 InterruptedError
resp = result_q.get()
finally:
with _cb_lock:
_cb_pending.pop(cb_id, None)
if resp is None or resp.get("type") == "error":
raise RuntimeError(resp.get("error", "go callback error") if resp else "connection closed")
return resp.get("data")
call_go = _CallGoType()
def _raise_in_thread(thread_id: int, exc_type: type) -> bool:
"""向指定线程注入异常(在下一条字节码指令时触发)。
@@ -115,11 +218,14 @@ class _ConnMux:
while True:
msg = _read_msg(self.conn)
if msg is None:
# 连接关闭:唤醒主循环,中断所有正在执行的函数
# 连接关闭:唤醒主循环,中断所有正在执行的函数,并唤醒所有 call_go 等待
self.call_q.put(None)
with self._lock:
for tid in self._active_tids.values():
_raise_in_thread(tid, InterruptedError)
with _cb_lock:
for q in _cb_pending.values():
q.put(None) # 通知 call_go 连接已关闭
return
t = msg.get("type")
if t == "call":
@@ -132,6 +238,13 @@ class _ConnMux:
tid = self._active_tids.get(mid)
if tid is not None:
_raise_in_thread(tid, InterruptedError)
elif t in ("callback_result", "error"):
# Go 对 call_go 的回复:按 id 路由到对应等待队列
mid = msg.get("id")
with _cb_lock:
q = _cb_pending.get(mid)
if q is not None:
q.put(msg)
def register(self, msg_id: int, thread_id: int):
with self._lock:
@@ -167,6 +280,8 @@ def _handle_conn(conn: socket.socket):
def _dispatch(mux: _ConnMux, msg: dict):
"""处理一条 call 消息"""
# 将当前连接的 mux 写入线程局部存储,供 call_go() 使用
_local.mux = mux
msg_id = msg["id"]
method = msg.get("method", "")
fn = _exposed.get(method)

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "gobridge"
version = "0.1.0"
version = "0.1.1"
description = "Python 端库,配合 Go 侧 gobridge 使用"
requires-python = ">=3.10"