import ctypes import json import os import sys from dataclasses import field, dataclass from typing import Optional, Dict, Any, List def get_lib_filename(base_name): if sys.platform == "win32": return f"{base_name}.dll" elif sys.platform == "darwin": return f"lib{base_name}.dylib" else: return f"lib{base_name}.so" @dataclass class FetchResultCard: text: str = "" path: List[str] = field(default_factory=list) @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'FetchResultCard': return cls( text=data.get("text", ""), path=data.get("path", []), ) @dataclass class FetchResult: event_id: int = 0 cards: List[FetchResultCard] = field(default_factory=list) @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'FetchResult': return cls( event_id=data.get("event_id", 0), cards=[ FetchResultCard.from_dict(card) for card in data.get("cards", []) ], ) class SekaiSyncLib: def __init__(self): self.lib = ctypes.CDLL(os.path.join(os.path.dirname(__file__), get_lib_filename("sekai_sync_lib"))) self._setup_signatures() def _setup_signatures(self): # proxy(x: *const c_char) self.lib.proxy.argtypes = [ctypes.c_char_p] self.lib.proxy.restype = None # pjsk_proxy(x: *const c_char) self.lib.pjsk_proxy.argtypes = [ctypes.c_char_p] self.lib.pjsk_proxy.restype = None # ghp(x: *const c_char) self.lib.ghp.argtypes = [ctypes.c_char_p] self.lib.ghp.restype = None # ghp(x: *const c_char) self.lib.log_level.argtypes = [ctypes.c_char_p] self.lib.log_level.restype = None # boot(cwd: *const c_char, name: *const c_char, as_path: *const c_char) -> c_int self.lib.boot.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p] self.lib.boot.restype = ctypes.c_int # stop() -> c_int self.lib.stop.argtypes = [] self.lib.stop.restype = ctypes.c_int # fetch_data() -> *mut c_char # ⚠️ 这里必须用 c_void_p,不能用 c_char_p,否则会丢失指针地址导致内存泄漏 self.lib.fetch_data.argtypes = [] self.lib.fetch_data.restype = ctypes.c_void_p # free_string(s: *mut c_char) self.lib.free_string.argtypes = [ctypes.c_void_p] self.lib.free_string.restype = None def _str_to_bytes(self, s: Optional[str]) -> bytes: """辅助函数:将 Python 字符串安全转换为 C 字符串字节""" if s is None: # 根据 Rust 端解析逻辑,传入空字符串将被视为 None return b"" return s.encode('utf-8') def set_proxy(self, proxy_url: Optional[str]): """设置代理,传 None 或空字符串可清除""" self.lib.proxy(self._str_to_bytes(proxy_url)) def set_pjsk_proxy(self, proxy_url: Optional[str]): """设置 pjsk_proxy,传 None 或空字符串可清除""" self.lib.pjsk_proxy(self._str_to_bytes(proxy_url)) def set_ghp(self, token: Optional[str]): """设置 GitHub Token,传 None 或空字符串可清除""" self.lib.ghp(self._str_to_bytes(token)) def set_log_level(self, log_evel: Optional[str]): """设置 Log level,传 None 或空字符串可清除""" self.lib.log_level(self._str_to_bytes(log_evel)) def boot(self, cwd: str, name: str, as_path: str) -> int: """ 启动主循环与初始化状态 """ return self.lib.boot( self._str_to_bytes(cwd), self._str_to_bytes(name), self._str_to_bytes(as_path) ) def stop(self) -> int: """ 停止主循环与清理状态 """ return self.lib.stop() def fetch_data(self) -> Optional[FetchResult]: # 1. 获取裸指针 ptr = self.lib.fetch_data() # 指针为空 (std::ptr::null_mut) if not ptr: return None try: # 2. 将 void_p 转换回 c_char_p 并读取里面的 bytes json_bytes = ctypes.cast(ptr, ctypes.c_char_p).value if not json_bytes: return None # 3. 解码并反序列化 JSON json_str = json_bytes.decode('utf-8') return FetchResult.from_dict(json.loads(json_str)) finally: # 4. 无论是否发生 JSON 解析异常,绝对保证释放 Rust 内存! self.lib.free_string(ptr) rlib = SekaiSyncLib()