use super::md::{MdApi, MdSpiCallbacks};
use super::pod::{
EkDepthMarketData, EkErrorField, EkInputOrder, EkInstrumentCommissionRate, EkInstrumentField,
EkInstrumentMarginRate, EkInstrumentStatus, EkInvestorPositionField, EkOrderField,
EkTradeField, EkTradingAccountField,
};
use super::td::{TdApi, TdSpiCallbacks};
use crate::driver::{MdEvent, TdEvent};
use crate::plugin::{CtpPlugin, CtpTransport};
use crate::wire::{
ErrorField, InputOrder, InputOrderAction, QryInstrumentCommissionRate, QryInstrumentMarginRate,
};
use std::ffi::c_void;
use std::path::PathBuf;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, Mutex, OnceLock, Weak};
pub(crate) struct Ingress {
plugin: OnceLock<Weak<CtpPlugin>>,
}
impl Ingress {
fn new() -> Self {
Self {
plugin: OnceLock::new(),
}
}
fn attach(&self, plugin: &Arc<CtpPlugin>) {
let _ = self.plugin.set(Arc::downgrade(plugin));
}
fn plugin(&self) -> Option<Arc<CtpPlugin>> {
self.plugin.get().and_then(Weak::upgrade)
}
}
unsafe fn with_ingress(ctx: *mut c_void, f: impl FnOnce(&Ingress)) {
if ctx.is_null() {
return;
}
let ing = unsafe { &*(ctx as *const Ingress) };
f(ing);
}
fn error_checked(p: *const EkErrorField) -> Option<ErrorField> {
if p.is_null() {
return None;
}
let e = unsafe { (*p).to_wire() };
if e.error_id == 0 { None } else { Some(e) }
}
fn error_raw(p: *const EkErrorField) -> Option<ErrorField> {
if p.is_null() {
return None;
}
Some(unsafe { (*p).to_wire() })
}
unsafe extern "C" fn md_on_front_connected(ctx: *mut c_void) {
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_md_event(MdEvent::FrontConnected);
}
})
}
}
unsafe extern "C" fn md_on_front_disconnected(ctx: *mut c_void, reason: std::ffi::c_int) {
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_md_event(MdEvent::FrontDisconnected(reason as i64));
}
})
}
}
unsafe extern "C" fn md_on_rsp_user_login(ctx: *mut c_void, error: *const EkErrorField) {
let error = error_checked(error);
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_md_event(MdEvent::RspUserLogin { error });
}
})
}
}
unsafe extern "C" fn md_on_rsp_error(ctx: *mut c_void, error: *const EkErrorField) {
if error.is_null() {
return;
}
let e = unsafe { (*error).to_wire() };
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_md_event(MdEvent::RspError(e));
}
})
}
}
unsafe extern "C" fn md_on_rsp_sub_market_data(ctx: *mut c_void, error: *const EkErrorField) {
let error = error_checked(error);
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_md_event(MdEvent::RspSubMarketData { error });
}
})
}
}
unsafe extern "C" fn md_on_rtn_depth_market_data(
ctx: *mut c_void,
depth: *const EkDepthMarketData,
) {
if depth.is_null() {
return;
}
let tick = unsafe { (*depth).to_wire() };
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_md_event(MdEvent::RtnDepthMarketData(tick));
}
})
}
}
static MD_CALLBACKS: MdSpiCallbacks = MdSpiCallbacks {
on_front_connected: Some(md_on_front_connected),
on_front_disconnected: Some(md_on_front_disconnected),
on_rsp_user_login: Some(md_on_rsp_user_login),
on_rsp_error: Some(md_on_rsp_error),
on_rsp_sub_market_data: Some(md_on_rsp_sub_market_data),
on_rtn_depth_market_data: Some(md_on_rtn_depth_market_data),
};
unsafe extern "C" fn td_on_front_connected(ctx: *mut c_void) {
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::FrontConnected);
}
})
}
}
unsafe extern "C" fn td_on_front_disconnected(ctx: *mut c_void, reason: std::ffi::c_int) {
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::FrontDisconnected(reason as i64));
}
})
}
}
unsafe extern "C" fn td_on_rsp_authenticate(ctx: *mut c_void, error: *const EkErrorField) {
let error = error_checked(error);
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::RspAuthenticate { error });
}
})
}
}
unsafe extern "C" fn td_on_rsp_user_login(
ctx: *mut c_void,
front_id: std::ffi::c_int,
session_id: std::ffi::c_int,
error: *const EkErrorField,
) {
let error = error_checked(error);
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::RspUserLogin {
error,
front_id: front_id as i64,
session_id: session_id as i64,
});
}
})
}
}
unsafe extern "C" fn td_on_rsp_settlement_info_confirm(
ctx: *mut c_void,
error: *const EkErrorField,
) {
let error = error_checked(error);
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::RspSettlementInfoConfirm { error });
}
})
}
}
unsafe extern "C" fn td_on_rsp_order_insert(
ctx: *mut c_void,
order: *const EkInputOrder,
error: *const EkErrorField,
) {
let error = error_raw(error);
let req = if order.is_null() {
None
} else {
Some(unsafe { (*order).to_wire() })
};
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::RspOrderInsert { error, req });
}
})
}
}
unsafe extern "C" fn td_on_rsp_order_action(ctx: *mut c_void, error: *const EkErrorField) {
let error = error_raw(error);
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::RspOrderAction { error });
}
})
}
}
unsafe extern "C" fn td_on_rsp_qry_instrument(
ctx: *mut c_void,
row: *const EkInstrumentField,
error: *const EkErrorField,
is_last: std::ffi::c_int,
) {
let row = if row.is_null() {
None
} else {
Some(unsafe { (*row).to_wire() })
};
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::RspQryInstrument {
last: is_last != 0,
row,
error: error_checked(error),
});
}
})
}
}
unsafe extern "C" fn td_on_rsp_qry_investor_position(
ctx: *mut c_void,
row: *const EkInvestorPositionField,
error: *const EkErrorField,
is_last: std::ffi::c_int,
) {
let row = if row.is_null() {
None
} else {
Some(unsafe { (*row).to_wire() })
};
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::RspQryInvestorPosition {
last: is_last != 0,
row,
error: error_checked(error),
});
}
})
}
}
unsafe extern "C" fn td_on_rsp_qry_trading_account(
ctx: *mut c_void,
row: *const EkTradingAccountField,
error: *const EkErrorField,
is_last: std::ffi::c_int,
) {
let row = if row.is_null() {
None
} else {
Some(unsafe { (*row).to_wire() })
};
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::RspQryTradingAccount {
last: is_last != 0,
row,
error: error_checked(error),
});
}
})
}
}
unsafe extern "C" fn td_on_rtn_order(ctx: *mut c_void, order: *const EkOrderField) {
if order.is_null() {
return;
}
let of = unsafe { (*order).to_wire() };
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::RtnOrder(of));
}
})
}
}
unsafe extern "C" fn td_on_rtn_trade(ctx: *mut c_void, trade: *const EkTradeField) {
if trade.is_null() {
return;
}
let tf = unsafe { (*trade).to_wire() };
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::RtnTrade(tf));
}
})
}
}
unsafe extern "C" fn td_on_rsp_qry_instrument_margin_rate(
ctx: *mut c_void,
row: *const EkInstrumentMarginRate,
error: *const EkErrorField,
is_last: std::ffi::c_int,
) {
let row = if row.is_null() {
None
} else {
Some(unsafe { (*row).to_wire() })
};
let error = error_checked(error);
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::RspQryInstrumentMarginRate {
row,
error,
last: is_last != 0,
});
}
})
}
}
unsafe extern "C" fn td_on_rsp_qry_instrument_commission_rate(
ctx: *mut c_void,
row: *const EkInstrumentCommissionRate,
error: *const EkErrorField,
is_last: std::ffi::c_int,
) {
let row = if row.is_null() {
None
} else {
Some(unsafe { (*row).to_wire() })
};
let error = error_checked(error);
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::RspQryInstrumentCommissionRate {
row,
error,
last: is_last != 0,
});
}
})
}
}
unsafe extern "C" fn td_on_rtn_instrument_status(
ctx: *mut c_void,
status: *const EkInstrumentStatus,
) {
if status.is_null() {
return;
}
let s = unsafe { (*status).to_wire() };
unsafe {
with_ingress(ctx, |ing| {
if let Some(p) = ing.plugin() {
p.on_td_event(TdEvent::RtnInstrumentStatus(s));
}
})
}
}
static TD_CALLBACKS: TdSpiCallbacks = TdSpiCallbacks {
on_front_connected: Some(td_on_front_connected),
on_front_disconnected: Some(td_on_front_disconnected),
on_rsp_authenticate: Some(td_on_rsp_authenticate),
on_rsp_user_login: Some(td_on_rsp_user_login),
on_rsp_settlement_info_confirm: Some(td_on_rsp_settlement_info_confirm),
on_rsp_order_insert: Some(td_on_rsp_order_insert),
on_rsp_order_action: Some(td_on_rsp_order_action),
on_rsp_qry_instrument: Some(td_on_rsp_qry_instrument),
on_rsp_qry_investor_position: Some(td_on_rsp_qry_investor_position),
on_rsp_qry_trading_account: Some(td_on_rsp_qry_trading_account),
on_rtn_order: Some(td_on_rtn_order),
on_rtn_trade: Some(td_on_rtn_trade),
on_rsp_qry_instrument_margin_rate: Some(td_on_rsp_qry_instrument_margin_rate),
on_rsp_qry_instrument_commission_rate: Some(td_on_rsp_qry_instrument_commission_rate),
on_rtn_instrument_status: Some(td_on_rtn_instrument_status),
};
pub struct NativeTransport {
md: Mutex<Option<MdApi>>,
td: Mutex<Option<TdApi>>,
md_reqid: AtomicI64,
td_reqid: AtomicI64,
flow_root: PathBuf,
ingress: Arc<Ingress>,
}
impl NativeTransport {
fn new(ingress: Arc<Ingress>, flow_root: impl Into<PathBuf>) -> Self {
Self {
md: Mutex::new(None),
td: Mutex::new(None),
md_reqid: AtomicI64::new(0),
td_reqid: AtomicI64::new(0),
flow_root: flow_root.into(),
ingress,
}
}
fn ingress_ptr(&self) -> *mut c_void {
Arc::as_ptr(&self.ingress) as *mut c_void
}
fn plugin(&self) -> Option<Arc<CtpPlugin>> {
self.ingress.plugin()
}
fn report(&self, what: &str, detail: &str) {
if let Some(p) = self.plugin() {
p.transport_log(format!("{what}失败:{detail}"));
}
}
fn next_md_reqid(&self) -> i32 {
self.md_reqid.fetch_add(1, Ordering::Relaxed) as i32 + 1
}
fn next_td_reqid(&self) -> i32 {
self.td_reqid.fetch_add(1, Ordering::Relaxed) as i32 + 1
}
fn prepare_flow_dir(&self, flow_path: &str) -> PathBuf {
let mut parts = flow_path.splitn(2, '\\');
let gw = parts.next().unwrap_or("ctp").to_lowercase();
let dir = match parts.next() {
Some(tail) if !tail.is_empty() => self.flow_root.join(gw).join(tail),
_ => self.flow_root.join(gw),
};
if let Err(e) = std::fs::create_dir_all(&dir) {
self.report("创建 CTP 流目录", &format!("{}({})", dir.display(), e));
}
dir
}
fn shutdown(&self) {
let md = self.md.lock().unwrap().take();
let td = self.td.lock().unwrap().take();
drop(md);
drop(td);
}
}
impl Drop for NativeTransport {
fn drop(&mut self) {
self.shutdown();
}
}
impl CtpTransport for NativeTransport {
fn td_release(&self) {
let td = self.td.lock().unwrap().take();
drop(td);
}
fn md_release(&self) {
let md = self.md.lock().unwrap().take();
drop(md);
}
fn md_connect(&self, addr: &str, flow_path: &str) {
let mut slot = self.md.lock().unwrap();
if slot.is_some() {
return;
}
let dir = self.prepare_flow_dir(flow_path);
match MdApi::new(&dir) {
Ok(mut api) => {
api.register_front(addr);
unsafe { api.register_spi(&MD_CALLBACKS, self.ingress_ptr()) };
api.init();
*slot = Some(api);
}
Err(e) => self.report("创建行情 API", &e),
}
}
fn md_login(&self, user: &str, password: &str, broker: &str) {
let mut slot = self.md.lock().unwrap();
if let Some(api) = slot.as_mut() {
let _ = api.req_user_login(broker, user, password, self.next_md_reqid());
}
}
fn md_subscribe(&self, symbols: &[String]) {
let mut slot = self.md.lock().unwrap();
let Some(api) = slot.as_mut() else { return };
for s in symbols {
let one = [s.as_str()];
let _ = api.subscribe(&one);
}
}
fn td_connect(&self, addr: &str, flow_path: &str) {
let mut slot = self.td.lock().unwrap();
if slot.is_some() {
return;
}
let dir = self.prepare_flow_dir(flow_path);
match TdApi::new(&dir) {
Ok(mut api) => {
api.subscribe_private_topic(super::td::TE_RESUME_RESTART);
api.subscribe_public_topic(super::td::TE_RESUME_RESTART);
api.register_front(addr);
unsafe { api.register_spi(&TD_CALLBACKS, self.ingress_ptr()) };
api.init();
*slot = Some(api);
}
Err(e) => self.report("创建交易 API", &e),
}
}
fn td_auth(&self, user: &str, broker: &str, auth_code: &str, app_id: &str) {
let mut slot = self.td.lock().unwrap();
if let Some(api) = slot.as_mut() {
let _ = api.req_authenticate(broker, user, auth_code, app_id, self.next_td_reqid());
}
}
fn td_login(&self, user: &str, password: &str, broker: &str) {
let mut slot = self.td.lock().unwrap();
if let Some(api) = slot.as_mut() {
let _ = api.req_user_login(broker, user, password, self.next_td_reqid());
}
}
fn td_settlement_confirm(&self, broker: &str, user: &str) {
let mut slot = self.td.lock().unwrap();
if let Some(api) = slot.as_mut() {
let _ = api.req_settlement_confirm(broker, user, self.next_td_reqid());
}
}
fn td_qry_instrument(&self) -> i32 {
let mut slot = self.td.lock().unwrap();
match slot.as_mut() {
Some(api) => api.req_qry_instrument(self.next_td_reqid()),
None => -1,
}
}
fn td_qry_account(&self, broker: &str, user: &str) -> i32 {
let mut slot = self.td.lock().unwrap();
match slot.as_mut() {
Some(api) => api.req_qry_trading_account(broker, user, self.next_td_reqid()),
None => -1,
}
}
fn td_qry_position(&self, broker: &str, user: &str) -> i32 {
let mut slot = self.td.lock().unwrap();
match slot.as_mut() {
Some(api) => api.req_qry_investor_position(broker, user, self.next_td_reqid()),
None => -1,
}
}
fn td_insert(&self, order: &InputOrder) -> i32 {
let (pod, _holders) = order.to_pod_cstr();
let mut slot = self.td.lock().unwrap();
match slot.as_mut() {
Some(api) => api.req_order_insert(&pod, self.next_td_reqid()),
None => -1,
}
}
fn td_cancel(&self, action: &InputOrderAction) -> i32 {
let (pod, _holders) = action.to_pod_cstr();
let mut slot = self.td.lock().unwrap();
match slot.as_mut() {
Some(api) => api.req_order_action(&pod, self.next_td_reqid()),
None => -1,
}
}
fn td_qry_margin_rate(&self, query: &QryInstrumentMarginRate) -> i32 {
let (pod, _holders) = query.to_pod_cstr();
let mut slot = self.td.lock().unwrap();
match slot.as_mut() {
Some(api) => api.req_qry_instrument_margin_rate(&pod, self.next_td_reqid()),
None => -1,
}
}
fn td_qry_commission_rate(&self, query: &QryInstrumentCommissionRate) -> i32 {
let (pod, _holders) = query.to_pod_cstr();
let mut slot = self.td.lock().unwrap();
match slot.as_mut() {
Some(api) => api.req_qry_instrument_commission_rate(&pod, self.next_td_reqid()),
None => -1,
}
}
fn shutdown(&self) {
NativeTransport::shutdown(self);
}
}
pub(crate) fn native_parts(flow_root: impl Into<PathBuf>) -> (Box<dyn CtpTransport>, Arc<Ingress>) {
let ingress = Arc::new(Ingress::new());
let trans = NativeTransport::new(ingress.clone(), flow_root);
(Box::new(trans), ingress)
}
pub(crate) fn attach_plugin(ingress: &Ingress, plugin: &Arc<CtpPlugin>) {
ingress.attach(plugin);
}
#[cfg(test)]
pub(crate) fn md_slot_filled(t: &NativeTransport) -> bool {
t.md.lock().unwrap().is_some()
}
#[cfg(test)]
pub(crate) fn test_transport(flow_root: impl Into<PathBuf>) -> NativeTransport {
NativeTransport::new(Arc::new(Ingress::new()), flow_root)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flow_dir_follows_original_layout() {
let t = test_transport("root");
assert_eq!(
t.prepare_flow_dir("Ctp\\Md"),
PathBuf::from("root").join("ctp").join("Md")
);
assert_eq!(
t.prepare_flow_dir("Ctp\\Td"),
PathBuf::from("root").join("ctp").join("Td")
);
assert_eq!(t.prepare_flow_dir("Ctp"), PathBuf::from("root").join("ctp"));
assert!(!md_slot_filled(&t));
}
#[test]
fn reqid_starts_at_one_and_is_per_session() {
let t = test_transport(std::env::temp_dir());
assert_eq!(t.next_md_reqid(), 1);
assert_eq!(t.next_md_reqid(), 2);
assert_eq!(t.next_td_reqid(), 1);
}
#[test]
fn callback_ingress_is_inert_without_ctx_or_plugin() {
unsafe { with_ingress(std::ptr::null_mut(), |_| panic!("空 ctx 不得执行回调体")) };
let ingress = Arc::new(Ingress::new());
assert!(ingress.plugin().is_none(), "未接线时不得有插件");
unsafe {
with_ingress(Arc::as_ptr(&ingress) as *mut c_void, |i| {
assert!(i.plugin().is_none(), "未接线时回调体应看到 None 并放弃")
})
};
}
}