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:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user