ek-ctp 0.2.0

CTP gateway for the enkeep ek ecosystem: MD/TD session drivers mapping CTP wire rows onto ek-data objects over the ek-event bus (pure-logic core; the CTP SDK ships inside this package and native bridging lands behind the optional `native` feature).
Documentation
//! C ABI 声明与 MD 面的安全包装。
//!
//! 这是 crate 里唯一的 `unsafe` 面:所有 `extern "C"` 声明与裸指针操作收在此处,
//! 上层只见安全类型。字段顺序/取值规则以 `native/shim/ek_ctp_shim.h` 为准,
//! 并由 `tests/native_smoke.rs` 的 `sizeof` 断言做保护。
//!
//! Rust 2024 起 `extern` 块必须写 `unsafe extern`,这里照办。

use super::common::{borrow_cstr, cstring, last_error};
use super::pod::{EkDepthMarketData, EkErrorField};
use std::ffi::{CString, c_char, c_int, c_void};
use std::path::Path;

/// shim 的 MD SPI 回调表 —— 字段顺序与 C 侧 `EkMdSpiCallbacks` 严格一致(ABI 契约)。
///
/// 只携带 `MdEvent` 真正消费的信息:原稿的 MD 登录/报错/订阅回报回调**只读 error**
/// (ctp_gateway.py 275-300),所以这里不搬运 login_time 之类的未用字段
/// —— 那是关于报文形状的、无人验证的断言(D-NATIVE-SURF)。行情报文的
/// `TradingDay` 是 0.2.1 起的记档偏差新增(trading_day 轴,见 DESIGN D-TZ)。
///
/// 回调可为 `None`(= C 侧空指针),shim 在调用前逐项判空,故"只装配不接网络"的
/// 场景可以传 [`MdSpiCallbacks::none`]。
#[repr(C)]
#[derive(Clone, Copy)]
pub struct MdSpiCallbacks {
    pub on_front_connected: Option<unsafe extern "C" fn(*mut c_void)>,
    pub on_front_disconnected: Option<unsafe extern "C" fn(*mut c_void, c_int)>,
    pub on_rsp_user_login: Option<unsafe extern "C" fn(*mut c_void, *const EkErrorField)>,
    pub on_rsp_error: Option<unsafe extern "C" fn(*mut c_void, *const EkErrorField)>,
    pub on_rsp_sub_market_data: Option<unsafe extern "C" fn(*mut c_void, *const EkErrorField)>,
    pub on_rtn_depth_market_data:
        Option<unsafe extern "C" fn(*mut c_void, *const EkDepthMarketData)>,
}

impl MdSpiCallbacks {
    /// 全空回调表(离线装配冒烟用)。
    pub const fn none() -> Self {
        Self {
            on_front_connected: None,
            on_front_disconnected: None,
            on_rsp_user_login: None,
            on_rsp_error: None,
            on_rsp_sub_market_data: None,
            on_rtn_depth_market_data: None,
        }
    }
}

unsafe extern "C" {
    fn ek_shim_md_api_version() -> *const c_char;
    fn ek_shim_sizeof_md_spi_callbacks() -> c_int;

    fn ek_shim_md_create(
        flow_path: *const c_char,
        use_udp: c_int,
        use_multicast: c_int,
    ) -> *mut c_void;
    fn ek_shim_md_release(handle: *mut c_void);

    fn ek_shim_md_register_front(handle: *mut c_void, front_address: *const c_char);
    fn ek_shim_md_register_spi(
        handle: *mut c_void,
        callbacks: *const MdSpiCallbacks,
        ctx: *mut c_void,
    );
    fn ek_shim_md_init(handle: *mut c_void);

    fn ek_shim_md_req_user_login(
        handle: *mut c_void,
        broker_id: *const c_char,
        user_id: *const c_char,
        password: *const c_char,
        request_id: c_int,
    ) -> c_int;
    fn ek_shim_md_subscribe(
        handle: *mut c_void,
        instrument_ids: *const *const c_char,
        count: c_int,
    ) -> c_int;
}

/// CTP API 版本串(由 DLL 内嵌字符串报告,如 `6.7.7`)。
pub fn api_version() -> Option<String> {
    // SAFETY: SDK 返回 NUL 结尾的静态版本串。
    let p = unsafe { ek_shim_md_api_version() };
    // SAFETY: 同上。
    unsafe { borrow_cstr(p) }
}

/// shim 侧 `sizeof(EkMdSpiCallbacks)` —— 与 Rust 的 `size_of` 比对可抓出 ABI 错位。
pub fn callbacks_sizeof() -> usize {
    unsafe { ek_shim_sizeof_md_spi_callbacks() }.max(0) as usize
}

/// 行情 API 句柄(RAII:drop 即调用 `Release`)。
///
/// 内含裸指针,默认非 `Send`/`Sync`。这里显式实现 [`Send`](见 DESIGN
/// D-NATIVE-THREADSAFE):CTP 的 api 方法可从任意线程调用(其内部对收发自行排队),
/// 而本 crate 的传输层把所有句柄访问串行化在一个 `Mutex` 里,不存在并发调用。
/// `Sync` **刻意不实现** —— 句柄必须独占,不允许多个线程同时持有同一句柄的引用。
pub struct MdApi {
    handle: *mut c_void,
}

// SAFETY: 见类型文档。所有访问都经传输层的 `Mutex` 串行化;CTP 允许请求调用与
// SPI 回调处在不同线程上(SPI 回调本来就在 SDK 自己的线程里)。该假设的端到端
// 复核在 M3 的 SimNow 实连(见 DESIGN D-NATIVE-THREADSAFE 的回退方案)。
unsafe impl Send for MdApi {}

impl MdApi {
    /// 创建行情 API。`flow_path` 是 CTP 私有流/公有流落盘目录,必须已存在且为 ASCII 路径。
    pub fn new(flow_path: &Path) -> Result<Self, String> {
        let path = flow_path
            .to_str()
            .ok_or_else(|| format!("flow path is not valid UTF-8: {}", flow_path.display()))?;
        let flow = cstring(path, "flow path");
        let handle = unsafe { ek_shim_md_create(flow.as_ptr(), 0, 0) };
        if handle.is_null() {
            return Err(
                last_error().unwrap_or_else(|| "ek_shim_md_create returned null".to_string())
            );
        }
        Ok(Self { handle })
    }

    /// 注册前置地址(不发起连接;连接由 [`MdApi::init`] 触发)。
    pub fn register_front(&mut self, front_address: &str) {
        let addr = cstring(front_address, "front address");
        unsafe { ek_shim_md_register_front(self.handle, addr.as_ptr()) };
    }

    /// 注册回调表。表按值拷入 C++ 侧对象,故传临时量也安全;`ctx` 的生命周期由调用方负责。
    ///
    /// # Safety
    ///
    /// `ctx` 会被 CTP 原样回传给回调,调用方必须保证:
    /// 1. 该指针在**本 API 句柄存活期内**始终有效(回调发生在 CTP 自己的线程上);
    /// 2. 回调函数对它提出的对齐/别名/线程安全要求都被满足。
    pub unsafe fn register_spi(&mut self, callbacks: &MdSpiCallbacks, ctx: *mut c_void) {
        // SAFETY: 由本函数的 Safety 契约保证 ctx 有效;callbacks 是有效引用且被按值拷贝。
        unsafe { ek_shim_md_register_spi(self.handle, callbacks, ctx) };
    }

    /// 启动连接(CTP 内部线程管理连接/重连/登录重放)。
    pub fn init(&mut self) {
        unsafe { ek_shim_md_init(self.handle) };
    }

    /// 请求登录,返回 CTP 原始返回码。
    pub fn req_user_login(
        &mut self,
        broker_id: &str,
        user_id: &str,
        password: &str,
        request_id: i32,
    ) -> i32 {
        let broker = cstring(broker_id, "broker id");
        let user = cstring(user_id, "user id");
        let pass = cstring(password, "password");
        unsafe {
            ek_shim_md_req_user_login(
                self.handle,
                broker.as_ptr(),
                user.as_ptr(),
                pass.as_ptr(),
                request_id as c_int,
            )
        }
    }

    /// 订阅行情,返回 CTP 原始返回码。
    pub fn subscribe(&mut self, instruments: &[&str]) -> i32 {
        let owned: Vec<CString> = instruments
            .iter()
            .map(|s| cstring(s, "instrument id"))
            .collect();
        let ptrs: Vec<*const c_char> = owned.iter().map(|c| c.as_ptr()).collect();
        unsafe { ek_shim_md_subscribe(self.handle, ptrs.as_ptr(), ptrs.len() as c_int) }
    }
}

impl Drop for MdApi {
    fn drop(&mut self) {
        if !self.handle.is_null() {
            unsafe { ek_shim_md_release(self.handle) };
            self.handle = std::ptr::null_mut();
        }
    }
}