Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **C/C++ SDKs:** every list argument that crosses the FFI boundary now tolerates a null pointer with a zero length. `std::vector::data()` is allowed to return `nullptr` for an empty vector, which is exactly what the C++ binding passes for an omitted list argument, but the C layer fed it straight to `std::slice::from_raw_parts` — undefined behaviour that **aborts the process** under the debug UB checks. Hit live by `QuoteContext::warrant_list` with no filters (`c/src/quote_context/context.rs`); all 17 call sites across `quote_context`, `trade_context`, `agent_context`, `alert_context`, and `types` now go through a null-tolerant `slice_from_raw_parts` helper

### Added

- **Rust:** `Signal.status` is now a `SignalStatus` enum (pending / active / deleted / ai-failed / filtered-by-manual / ai-submit-failed), `SignalsResponse.total` is `i32` to match the wire contract, and the `risk_level` / `display_control` fields were dropped — neither is part of the API contract nor served in production
Expand Down
6 changes: 3 additions & 3 deletions c/src/agent_context/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
async_call::{CAsyncCallback, execute_async},
callback::CFreeUserDataFunc,
config::CConfig,
types::{CCow, ToFFI, cstr_to_rust},
types::{CCow, ToFFI, cstr_to_rust, slice_from_raw_parts},
};

/// AI Agent conversation context
Expand Down Expand Up @@ -65,10 +65,10 @@ unsafe fn answers_from_ffi(
num_answers: usize,
) -> AnswersByToolCall {
let mut map: AnswersByToolCall = HashMap::new();
for entry in std::slice::from_raw_parts(answers, num_answers) {
for entry in slice_from_raw_parts(answers, num_answers) {
let tool_call_id = cstr_to_rust(entry.tool_call_id);
let mut questions = HashMap::new();
for qa in std::slice::from_raw_parts(entry.answers, entry.num_answers) {
for qa in slice_from_raw_parts(entry.answers, entry.num_answers) {
questions.insert(cstr_to_rust(qa.question), cstr_to_rust(qa.answer));
}
map.insert(tool_call_id, questions);
Expand Down
4 changes: 2 additions & 2 deletions c/src/alert_context/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::os::raw::c_char;

use longbridge::alert::{AlertItem, AlertList, AlertSymbolGroup};

use crate::types::{CString, CVec, ToFFI};
use crate::types::{CString, CVec, ToFFI, slice_from_raw_parts};

/// A single alert indicator configuration for a symbol.
#[repr(C)]
Expand Down Expand Up @@ -63,7 +63,7 @@ impl CAlertItem {
/// `state` pointer must point to at least `num_state` valid `i32` values.
pub unsafe fn to_alert_item(&self) -> longbridge::alert::AlertItem {
use crate::types::cstr_to_rust;
let state = std::slice::from_raw_parts(self.state, self.num_state).to_vec();
let state = slice_from_raw_parts(self.state, self.num_state).to_vec();
let value_map_str = cstr_to_rust(self.value_map);
let value_map = serde_json::from_str(&value_map_str).unwrap_or(serde_json::Value::Null);
longbridge::alert::AlertItem {
Expand Down
21 changes: 12 additions & 9 deletions c/src/quote_context/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ use crate::{
LB_WATCHLIST_GROUP_NAME, LB_WATCHLIST_GROUP_SECURITIES,
},
},
types::{CCow, CDate, CDateTime, CMarket, CVec, ToFFI, cstr_array_to_rust, cstr_to_rust},
types::{
CCow, CDate, CDateTime, CMarket, CVec, ToFFI, cstr_array_to_rust, cstr_to_rust,
slice_from_raw_parts,
},
};

pub type COnQuoteCallback = extern "C" fn(*const CQuoteContext, *const CPushQuote, *mut c_void);
Expand Down Expand Up @@ -781,23 +784,23 @@ pub unsafe extern "C" fn lb_quote_context_warrant_list(
let symbol = cstr_to_rust(symbol);
let sort_by = sort_by.into();
let sort_order = sort_order.into();
let warrant_type = std::slice::from_raw_parts(warrant_type, num_warrant_type)
let warrant_type = slice_from_raw_parts(warrant_type, num_warrant_type)
.iter()
.copied()
.map(Into::into)
.collect::<Vec<_>>();
let issuer = std::slice::from_raw_parts(issuer, num_issuer).to_vec();
let expiry_date = std::slice::from_raw_parts(expiry_date, num_expiry_date)
let issuer = slice_from_raw_parts(issuer, num_issuer).to_vec();
let expiry_date = slice_from_raw_parts(expiry_date, num_expiry_date)
.iter()
.copied()
.map(Into::into)
.collect::<Vec<_>>();
let price_type = std::slice::from_raw_parts(price_type, num_price_type)
let price_type = slice_from_raw_parts(price_type, num_price_type)
.iter()
.copied()
.map(Into::into)
.collect::<Vec<_>>();
let status = std::slice::from_raw_parts(status, num_status)
let status = slice_from_raw_parts(status, num_status)
.iter()
.copied()
.map(Into::into)
Expand Down Expand Up @@ -904,7 +907,7 @@ pub unsafe extern "C" fn lb_quote_context_calc_indexes(
) {
let ctx_inner = (*ctx).ctx.clone();
let symbols = cstr_array_to_rust(symbols, num_symbols);
let indexes = std::slice::from_raw_parts(indexes, num_indexes)
let indexes = slice_from_raw_parts(indexes, num_indexes)
.iter()
.map(|index| (*index).into())
.collect::<Vec<_>>();
Expand Down Expand Up @@ -939,7 +942,7 @@ pub unsafe extern "C" fn lb_quote_context_create_watchlist_group(
) {
let ctx_inner = (*ctx).ctx.clone();
let name = cstr_to_rust(req.name);
let securities = std::slice::from_raw_parts(req.securities, req.num_securities);
let securities = slice_from_raw_parts(req.securities, req.num_securities);
let securities = (req.num_securities > 0).then(|| {
securities
.iter()
Expand Down Expand Up @@ -1003,7 +1006,7 @@ pub unsafe extern "C" fn lb_quote_context_update_watchlist_group(
let ctx_inner = (*ctx).ctx.clone();
let id = req.id;
let name = ((req.flags & LB_WATCHLIST_GROUP_NAME) != 0).then(|| cstr_to_rust(req.name));
let securities = std::slice::from_raw_parts(req.securities, req.num_securities);
let securities = slice_from_raw_parts(req.securities, req.num_securities);
let securities = ((req.flags & LB_WATCHLIST_GROUP_SECURITIES) != 0).then(|| {
securities
.iter()
Expand Down
12 changes: 6 additions & 6 deletions c/src/trade_context/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use crate::{
CSubmitMultiLegOrderOptions, CSubmitOrderOptions, CSubmitOrderResponseOwned,
},
},
types::{CCow, CVec, ToFFI, cstr_array_to_rust, cstr_to_rust},
types::{CCow, CVec, ToFFI, cstr_array_to_rust, cstr_to_rust, slice_from_raw_parts},
};

pub type COnOrderChangedCallback =
Expand Down Expand Up @@ -221,7 +221,7 @@ pub unsafe extern "C" fn lb_trade_context_subscribe(
userdata: *mut c_void,
) {
let ctx_inner = (*ctx).ctx.clone();
let topics = std::slice::from_raw_parts(topics, num_topics)
let topics = slice_from_raw_parts(topics, num_topics)
.iter()
.copied()
.map(Into::into)
Expand All @@ -240,7 +240,7 @@ pub unsafe extern "C" fn lb_trade_context_unsubscribe(
userdata: *mut c_void,
) {
let ctx_inner = (*ctx).ctx.clone();
let topics = std::slice::from_raw_parts(topics, num_topics)
let topics = slice_from_raw_parts(topics, num_topics)
.iter()
.copied()
.map(Into::into)
Expand Down Expand Up @@ -366,7 +366,7 @@ pub unsafe extern "C" fn lb_trade_context_history_orders(
opts2 = opts2.symbol(cstr_to_rust((*opts).symbol));
}
if !(*opts).status.is_null() {
let status = std::slice::from_raw_parts((*opts).status, (*opts).num_status);
let status = slice_from_raw_parts((*opts).status, (*opts).num_status);
opts2 = opts2.status(status.iter().copied().map(Into::into));
}
if !(*opts).side.is_null() {
Expand Down Expand Up @@ -409,7 +409,7 @@ pub unsafe extern "C" fn lb_trade_context_today_orders(
opts2 = opts2.symbol(cstr_to_rust((*opts).symbol));
}
if !(*opts).status.is_null() {
let status = std::slice::from_raw_parts((*opts).status, (*opts).num_status);
let status = slice_from_raw_parts((*opts).status, (*opts).num_status);
opts2 = opts2.status(status.iter().copied().map(Into::into));
}
if !(*opts).side.is_null() {
Expand Down Expand Up @@ -638,7 +638,7 @@ pub unsafe extern "C" fn lb_trade_context_submit_multileg(
let order_type = (*opts).order_type.into();
let submitted_quantity = (*(*opts).submitted_quantity).value;
let strategy = (*opts).strategy.into();
let legs = std::slice::from_raw_parts((*opts).legs, (*opts).num_legs)
let legs = slice_from_raw_parts((*opts).legs, (*opts).num_legs)
.iter()
.map(|leg| {
SubmitMultiLegOrderLeg::new(cstr_to_rust(leg.symbol), (*leg.ratio_quantity).value)
Expand Down
18 changes: 17 additions & 1 deletion c/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,24 @@ pub(crate) unsafe fn cstr_to_rust(value: *const c_char) -> String {
.expect("invalid cstr")
}

/// Like [`std::slice::from_raw_parts`], but tolerates a null `data` pointer
/// when `len` is `0`.
///
/// `from_raw_parts` requires a non-null, aligned pointer even for a zero-length
/// slice, but `std::vector::data()` in C++ is allowed to return `nullptr` for
/// an empty vector — which is exactly what the C++ binding passes for an
/// omitted list argument. Calling `from_raw_parts(null, 0)` is undefined
/// behaviour and aborts the process under the debug UB checks.
pub(crate) unsafe fn slice_from_raw_parts<'a, T>(data: *const T, len: usize) -> &'a [T] {
if len == 0 || data.is_null() {
&[]
} else {
std::slice::from_raw_parts(data, len)
}
}

pub(crate) unsafe fn cstr_array_to_rust(values: *const *const c_char, n: usize) -> Vec<String> {
std::slice::from_raw_parts(values, n)
slice_from_raw_parts(values, n)
.iter()
.copied()
.map(|value| cstr_to_rust(value))
Expand Down
Loading