ek-ctp 0.11.2

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 声明与 TD 面的安全包装(交易 API、SPI 回调表、请求面)。
//!
//! 与 [`super::md`] 同构:所有裸指针操作收在此处,上层只见安全类型;字段顺序以
//! `native/shim/ek_ctp_shim.h` 的 `EkTdSpiCallbacks` 为准,并由
//! `tests/native_smoke.rs` 的 `sizeof` 断言保护。
//!
//! 回调表只携带 `TdEvent` 真正消费的信息(D-NATIVE-SURF):原稿的认证/结算确认/
//! 撤单回报回调都只读 error,查询回报只读行数据 + `is_last`,登录回报额外需要
//! `FrontID`/`SessionID`(原稿 477-478 以此拼委托号)。
//!
//! Rust 2024 起 `extern` 块必须写 `unsafe extern`,这里照办。

use super::common::{borrow_cstr, cstring, last_error};
use super::pod::{
    EkErrorField, EkInputOrder, EkInputOrderAction, EkInstrumentCommissionRate, EkInstrumentField,
    EkInstrumentMarginRate, EkInstrumentStatus, EkInvestorPositionField, EkOrderField,
    EkQryInstrumentCommissionRate, EkQryInstrumentMarginRate, EkTradeField, EkTradingAccountField,
};
use std::ffi::{c_char, c_int, c_void};
use std::path::Path;

/// shim 的 TD SPI 回调表 —— 字段顺序与 C 侧 `EkTdSpiCallbacks` 严格一致(ABI 契约)。
///
/// 12 个回调 = 原稿 `CtpTdApi` 实现的 12 个 TraderSpi 覆写。`error` 为空指针表示
/// SDK 根本没给 info 结构;非空但 `error_id == 0` 表示成功 —— 该判定在 Rust 侧做
/// (D-NATIVE-ERR),shim 不替我们下"失败"结论。
#[repr(C)]
#[derive(Clone, Copy)]
pub struct TdSpiCallbacks {
    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_authenticate: Option<unsafe extern "C" fn(*mut c_void, *const EkErrorField)>,
    pub on_rsp_user_login:
        Option<unsafe extern "C" fn(*mut c_void, c_int, c_int, *const EkErrorField)>,
    pub on_rsp_settlement_info_confirm:
        Option<unsafe extern "C" fn(*mut c_void, *const EkErrorField)>,
    pub on_rsp_order_insert:
        Option<unsafe extern "C" fn(*mut c_void, *const EkInputOrder, *const EkErrorField)>,
    pub on_rsp_order_action: Option<unsafe extern "C" fn(*mut c_void, *const EkErrorField)>,
    // 0.9.1:合约查询也携带错误面 —— 三条查询路(合约/持仓/资金)至此同式。
    pub on_rsp_qry_instrument: Option<
        unsafe extern "C" fn(*mut c_void, *const EkInstrumentField, *const EkErrorField, c_int),
    >,
    // 0.9.0:轮转两查询携带错误面(与 shim 头一字不差;字段顺序 = shim 结构体顺序,
    // 这里错一位就是把回调接到别的槽上 —— 编译期类型不匹配是唯一护栏)。
    pub on_rsp_qry_investor_position: Option<
        unsafe extern "C" fn(
            *mut c_void,
            *const EkInvestorPositionField,
            *const EkErrorField,
            c_int,
        ),
    >,
    pub on_rsp_qry_trading_account: Option<
        unsafe extern "C" fn(*mut c_void, *const EkTradingAccountField, *const EkErrorField, c_int),
    >,
    pub on_rtn_order: Option<unsafe extern "C" fn(*mut c_void, *const EkOrderField)>,
    pub on_rtn_trade: Option<unsafe extern "C" fn(*mut c_void, *const EkTradeField)>,
    // ---- 附加接口面(超出原稿):三路均携带错误面 ----
    // (0.9.0 起轮转两查询也携带;`on_rsp_qry_instrument` 是唯一还不带的查询 —— 刻意划界)
    pub on_rsp_qry_instrument_margin_rate: Option<
        unsafe extern "C" fn(
            *mut c_void,
            *const EkInstrumentMarginRate,
            *const EkErrorField,
            c_int,
        ),
    >,
    pub on_rsp_qry_instrument_commission_rate: Option<
        unsafe extern "C" fn(
            *mut c_void,
            *const EkInstrumentCommissionRate,
            *const EkErrorField,
            c_int,
        ),
    >,
    pub on_rtn_instrument_status:
        Option<unsafe extern "C" fn(*mut c_void, *const EkInstrumentStatus)>,
}

impl TdSpiCallbacks {
    /// 全空回调表(离线装配冒烟用)。
    pub const fn none() -> Self {
        Self {
            on_front_connected: None,
            on_front_disconnected: None,
            on_rsp_authenticate: None,
            on_rsp_user_login: None,
            on_rsp_settlement_info_confirm: None,
            on_rsp_order_insert: None,
            on_rsp_order_action: None,
            on_rsp_qry_instrument: None,
            on_rsp_qry_investor_position: None,
            on_rsp_qry_trading_account: None,
            on_rtn_order: None,
            on_rtn_trade: None,
            on_rsp_qry_instrument_margin_rate: None,
            on_rsp_qry_instrument_commission_rate: None,
            on_rtn_instrument_status: None,
        }
    }
}

unsafe extern "C" {
    fn ek_shim_td_api_version() -> *const c_char;
    fn ek_shim_sizeof_td_spi_callbacks() -> c_int;

    fn ek_shim_td_create(flow_path: *const c_char) -> *mut c_void;
    fn ek_shim_td_release(handle: *mut c_void);

    fn ek_shim_td_register_front(handle: *mut c_void, front_address: *const c_char);
    fn ek_shim_td_register_spi(
        handle: *mut c_void,
        callbacks: *const TdSpiCallbacks,
        ctx: *mut c_void,
    );
    fn ek_shim_td_subscribe_private_topic(handle: *mut c_void, resume_type: c_int);
    fn ek_shim_td_subscribe_public_topic(handle: *mut c_void, resume_type: c_int);
    fn ek_shim_td_init(handle: *mut c_void);

    fn ek_shim_td_req_authenticate(
        handle: *mut c_void,
        broker_id: *const c_char,
        user_id: *const c_char,
        auth_code: *const c_char,
        app_id: *const c_char,
        request_id: c_int,
    ) -> c_int;
    fn ek_shim_td_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_td_req_settlement_confirm(
        handle: *mut c_void,
        broker_id: *const c_char,
        investor_id: *const c_char,
        request_id: c_int,
    ) -> c_int;
    fn ek_shim_td_req_qry_instrument(handle: *mut c_void, request_id: c_int) -> c_int;
    fn ek_shim_td_req_qry_trading_account(
        handle: *mut c_void,
        broker_id: *const c_char,
        investor_id: *const c_char,
        request_id: c_int,
    ) -> c_int;
    fn ek_shim_td_req_qry_investor_position(
        handle: *mut c_void,
        broker_id: *const c_char,
        investor_id: *const c_char,
        request_id: c_int,
    ) -> c_int;
    fn ek_shim_td_req_order_insert(
        handle: *mut c_void,
        order: *const EkInputOrder,
        request_id: c_int,
    ) -> c_int;
    fn ek_shim_td_req_order_action(
        handle: *mut c_void,
        action: *const EkInputOrderAction,
        request_id: c_int,
    ) -> c_int;
    // ---- 附加接口面(超出原稿):两个查询的 Req(status 无对应 Req)----
    fn ek_shim_td_req_qry_instrument_margin_rate(
        handle: *mut c_void,
        query: *const EkQryInstrumentMarginRate,
        request_id: c_int,
    ) -> c_int;
    fn ek_shim_td_req_qry_instrument_commission_rate(
        handle: *mut c_void,
        query: *const EkQryInstrumentCommissionRate,
        request_id: c_int,
    ) -> c_int;
}

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

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

/// `THOST_TERT_RESTART`:原稿 `subscribePrivateTopic(0)` / `subscribePublicTopic(0)`
/// 的那个 0(ThostFtdcUserApiDataType.h 的枚举值)。命名出来,免得调用点出现魔法数。
pub const TE_RESUME_RESTART: i32 = 0;

/// 交易 API 句柄(RAII:drop 即调用 `Release`)。
///
/// 与 [`super::md::MdApi`] 同样只实现 `Send`、不实现 `Sync`,理由见其文档
/// (DESIGN D-NATIVE-THREADSAFE)。
pub struct TdApi {
    handle: *mut c_void,
}

// SAFETY: 同 MdApi —— 访问一律经传输层的 `Mutex` 串行化。
unsafe impl Send for TdApi {}

impl TdApi {
    /// 创建交易 API。`flow_path` 是 CTP 私有流/公有流落盘目录,必须已存在。
    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_td_create(flow.as_ptr()) };
        if handle.is_null() {
            return Err(last_error().unwrap_or_else(|| "ek_shim_td_create returned null".into()));
        }
        Ok(Self { handle })
    }

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

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

    /// 订阅私有流(原稿传 0 = [`TE_RESUME_RESTART`])。必须在 `init` 之前调用。
    pub fn subscribe_private_topic(&mut self, resume_type: i32) {
        unsafe { ek_shim_td_subscribe_private_topic(self.handle, resume_type as c_int) };
    }

    /// 订阅公有流(原稿传 0 = [`TE_RESUME_RESTART`])。必须在 `init` 之前调用。
    pub fn subscribe_public_topic(&mut self, resume_type: i32) {
        unsafe { ek_shim_td_subscribe_public_topic(self.handle, resume_type as c_int) };
    }

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

    /// 客户端认证(`UserProductInfo` 保持空 —— 原稿请求字典只有 4 个键)。
    pub fn req_authenticate(
        &mut self,
        broker_id: &str,
        user_id: &str,
        auth_code: &str,
        app_id: &str,
        request_id: i32,
    ) -> i32 {
        let broker = cstring(broker_id, "broker id");
        let user = cstring(user_id, "user id");
        let auth = cstring(auth_code, "auth code");
        let app = cstring(app_id, "app id");
        unsafe {
            ek_shim_td_req_authenticate(
                self.handle,
                broker.as_ptr(),
                user.as_ptr(),
                auth.as_ptr(),
                app.as_ptr(),
                request_id as c_int,
            )
        }
    }

    /// 请求登录。
    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_td_req_user_login(
                self.handle,
                broker.as_ptr(),
                user.as_ptr(),
                pass.as_ptr(),
                request_id as c_int,
            )
        }
    }

    /// 结算单确认(`InvestorID` = 原稿的 `self.userid`)。
    pub fn req_settlement_confirm(
        &mut self,
        broker_id: &str,
        investor_id: &str,
        request_id: i32,
    ) -> i32 {
        let broker = cstring(broker_id, "broker id");
        let investor = cstring(investor_id, "investor id");
        unsafe {
            ek_shim_td_req_settlement_confirm(
                self.handle,
                broker.as_ptr(),
                investor.as_ptr(),
                request_id as c_int,
            )
        }
    }

    /// 合约查询(空请求 = 全市场)。**返回码必须回传**:原稿以它为条件做流控重发
    /// (ctp_gateway.py 519-533),重发循环住传输层(D-FLCN)。
    pub fn req_qry_instrument(&mut self, request_id: i32) -> i32 {
        unsafe { ek_shim_td_req_qry_instrument(self.handle, request_id as c_int) }
    }

    /// 资金查询。
    pub fn req_qry_trading_account(
        &mut self,
        broker_id: &str,
        investor_id: &str,
        request_id: i32,
    ) -> i32 {
        let broker = cstring(broker_id, "broker id");
        let investor = cstring(investor_id, "investor id");
        unsafe {
            ek_shim_td_req_qry_trading_account(
                self.handle,
                broker.as_ptr(),
                investor.as_ptr(),
                request_id as c_int,
            )
        }
    }

    /// 持仓查询。
    pub fn req_qry_investor_position(
        &mut self,
        broker_id: &str,
        investor_id: &str,
        request_id: i32,
    ) -> i32 {
        let broker = cstring(broker_id, "broker id");
        let investor = cstring(investor_id, "investor id");
        unsafe {
            ek_shim_td_req_qry_investor_position(
                self.handle,
                broker.as_ptr(),
                investor.as_ptr(),
                request_id as c_int,
            )
        }
    }

    /// 报单录入;返回 `ReqOrderInsert` 的原始返回码(原稿 846-848 以它判发送失败)。
    pub fn req_order_insert(&mut self, order: &EkInputOrder, request_id: i32) -> i32 {
        unsafe { ek_shim_td_req_order_insert(self.handle, order, request_id as c_int) }
    }

    /// 报单操作(撤单);返回 `ReqOrderAction` 的原始返回码。
    pub fn req_order_action(&mut self, action: &EkInputOrderAction, request_id: i32) -> i32 {
        unsafe { ek_shim_td_req_order_action(self.handle, action, request_id as c_int) }
    }

    /// 合约保证金率查询(附加接口面,超出原稿;返回码必须回传,流控同上)。
    pub fn req_qry_instrument_margin_rate(
        &mut self,
        query: &EkQryInstrumentMarginRate,
        request_id: i32,
    ) -> i32 {
        unsafe {
            ek_shim_td_req_qry_instrument_margin_rate(self.handle, query, request_id as c_int)
        }
    }

    /// 合约手续费率查询(附加接口面,超出原稿)。
    pub fn req_qry_instrument_commission_rate(
        &mut self,
        query: &EkQryInstrumentCommissionRate,
        request_id: i32,
    ) -> i32 {
        unsafe {
            ek_shim_td_req_qry_instrument_commission_rate(self.handle, query, request_id as c_int)
        }
    }
}

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