Skip to content
Open
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
14 changes: 14 additions & 0 deletions baml_language/crates/bridge_cffi/src/ffi/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,17 @@ pub extern "C" fn clone_handle(key: u64) -> u64 {
pub extern "C" fn release_handle(key: u64) {
HANDLE_TABLE.release(key);
}

/// Release multiple handles in one call. Ignores null pointers.
///
/// # Safety
///
/// `keys` must be valid for reads of `len` elements.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn release_handles(keys: *const u64, len: usize) {
if keys.is_null() || len == 0 {
return;
}
let slice = unsafe { std::slice::from_raw_parts(keys, len) };
HANDLE_TABLE.release_many(slice.iter().copied());
}
2 changes: 1 addition & 1 deletion baml_language/crates/bridge_cffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub use ffi::{
call_function_from_c, call_function_parse_from_c, call_function_stream_from_c,
cancel_function_call,
},
handle::{clone_handle, release_handle},
handle::{clone_handle, release_handle, release_handles},
objects::free_buffer,
runtime::{create_baml_runtime, destroy_baml_runtime, invoke_runtime_cli, version},
};
Expand Down
7 changes: 7 additions & 0 deletions baml_language/crates/bridge_ctypes/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ pub enum CtypesError {
#[error("Invalid handle key: {0}")]
InvalidHandleKey(u64),

#[error("Handle type mismatch for key {key}: expected {expected}, actual {actual}")]
InvalidHandleType {
key: u64,
expected: i32,
actual: i32,
},

#[error("Map entry missing key")]
MapEntryMissingKey,

Expand Down
46 changes: 46 additions & 0 deletions baml_language/crates/bridge_ctypes/src/handle_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ pub enum HandleTableValue {
Adt(BexExternalAdt),
}

const DEFAULT_MAX_INLINE_MEDIA_BYTES: usize = 1_000_000;

pub struct HandleTableOptions<'a> {
pub(crate) table: &'a HandleTable,
pub(crate) serialize_media: bool,
pub(crate) serialize_prompt_ast: bool,
pub(crate) max_inline_media_bytes: Option<usize>,
}

impl HandleTableOptions<'_> {
Expand All @@ -35,6 +38,7 @@ impl HandleTableOptions<'_> {
table: &HANDLE_TABLE,
serialize_media: true,
serialize_prompt_ast: true,
max_inline_media_bytes: Some(DEFAULT_MAX_INLINE_MEDIA_BYTES),
}
}

Expand All @@ -43,6 +47,7 @@ impl HandleTableOptions<'_> {
table: &HANDLE_TABLE,
serialize_media: false,
serialize_prompt_ast: false,
max_inline_media_bytes: None,
}
}
}
Expand Down Expand Up @@ -101,6 +106,33 @@ impl TryFrom<BexExternalValue> for HandleTableValue {
}
}

impl TryFrom<&BexExternalValue> for HandleTableValue {
type Error = &'static str;

fn try_from(value: &BexExternalValue) -> Result<Self, Self::Error> {
match value {
BexExternalValue::Handle(h) => Ok(Self::Handle(h.clone())),
BexExternalValue::Resource(r) => Ok(Self::Resource(r.clone())),
BexExternalValue::FunctionRef { global_index } => Ok(Self::FunctionRef {
global_index: *global_index,
}),
BexExternalValue::Adt(a) => Ok(Self::Adt(a.clone())),
BexExternalValue::Null
| BexExternalValue::Int(_)
| BexExternalValue::Float(_)
| BexExternalValue::Bool(_)
| BexExternalValue::String(_)
| BexExternalValue::Array { .. }
| BexExternalValue::Map { .. }
| BexExternalValue::Instance { .. }
| BexExternalValue::Variant { .. }
| BexExternalValue::Union { .. } => {
Err("only opaque BexExternalValue variants can be held as handles")
}
}
}
}

impl From<HandleTableValue> for BexExternalValue {
fn from(value: HandleTableValue) -> Self {
match value {
Expand Down Expand Up @@ -173,6 +205,20 @@ impl HandleTable {
.remove(&key)
.is_some()
}

/// Release multiple handles.
pub fn release_many<I>(&self, keys: I)
where
I: IntoIterator<Item = u64>,
{
let mut entries = self
.entries
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for key in keys {
entries.remove(&key);
}
}
Comment on lines +209 to +221
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add a dedicated unit test for release_many behavior.

The method is straightforward, but this is new cleanup-critical API surface and should have explicit coverage (present keys removed, missing keys ignored, duplicates tolerated).

🧪 Suggested unit test
 #[test]
 fn double_release_returns_false() {
@@
 }

+#[test]
+fn release_many_releases_present_and_ignores_missing() {
+    let table = HandleTable::new();
+    let key1 = table.insert(make_function_ref());
+    let key2 = table.insert(make_function_ref());
+
+    table.release_many([key1, 9_999_999, key2, key1]);
+
+    assert!(table.resolve(key1).is_none());
+    assert!(table.resolve(key2).is_none());
+}

As per coding guidelines: "Prefer writing Rust unit tests over integration tests where possible."

}

impl Default for HandleTable {
Expand Down
75 changes: 73 additions & 2 deletions baml_language/crates/bridge_ctypes/src/value_decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use indexmap::IndexMap;

use crate::{
baml::cffi::{
InboundClassValue, InboundEnumValue, InboundListValue, InboundMapEntry, InboundMapValue,
InboundValue, inbound_value::Value as InboundValueVariant,
BamlHandleType, InboundClassValue, InboundEnumValue, InboundListValue, InboundMapEntry,
InboundMapValue, InboundValue, inbound_value::Value as InboundValueVariant,
},
error::CtypesError,
handle_table::HandleTable,
Expand Down Expand Up @@ -39,12 +39,27 @@ pub fn inbound_to_external(
let value = handle_table
.resolve(handle.key)
.ok_or(CtypesError::InvalidHandleKey(handle.key))?;
let actual = value.handle_type() as i32;
validate_handle_type(handle.key, handle.handle_type, actual)?;
Ok(BexExternalValue::from((*value).clone()))
}
},
}
}

fn validate_handle_type(key: u64, expected: i32, actual: i32) -> Result<(), CtypesError> {
match BamlHandleType::try_from(expected) {
Ok(BamlHandleType::HandleUnspecified | BamlHandleType::HandleUnknown) => Ok(()),
Ok(_) if expected == actual => Ok(()),
Err(_) => Ok(()),
Ok(_) => Err(CtypesError::InvalidHandleType {
key,
expected,
actual,
}),
}
}

fn convert_list(
list: InboundListValue,
handle_table: &HandleTable,
Expand Down Expand Up @@ -136,3 +151,59 @@ pub fn kwargs_to_bex_values(
}
Ok(result)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{
baml::cffi::{BamlHandle, inbound_value::Value as InboundValueVariant},
handle_table::HandleTableValue,
};

fn inbound_handle(key: u64, handle_type: BamlHandleType) -> InboundValue {
InboundValue {
value: Some(InboundValueVariant::Handle(BamlHandle {
key,
handle_type: handle_type as i32,
})),
}
}

#[test]
fn handle_type_matches() {
let table = HandleTable::new();
let key = table.insert(HandleTableValue::FunctionRef { global_index: 1 });
let inbound = inbound_handle(key, BamlHandleType::FunctionRef);
let out = inbound_to_external(inbound, &table).unwrap();
assert!(matches!(
out,
BexExternalValue::FunctionRef { global_index: 1 }
));
}

#[test]
fn handle_type_unknown_is_allowed() {
let table = HandleTable::new();
let key = table.insert(HandleTableValue::FunctionRef { global_index: 2 });
let inbound = inbound_handle(key, BamlHandleType::HandleUnknown);
let out = inbound_to_external(inbound, &table).unwrap();
assert!(matches!(
out,
BexExternalValue::FunctionRef { global_index: 2 }
));
}

#[test]
fn handle_type_mismatch_errors() {
let table = HandleTable::new();
let key = table.insert(HandleTableValue::FunctionRef { global_index: 3 });
let inbound = inbound_handle(key, BamlHandleType::AdtMediaImage);
let err = inbound_to_external(inbound, &table).unwrap_err();
match err {
CtypesError::InvalidHandleType { key: err_key, .. } => {
assert_eq!(err_key, key);
}
_ => panic!("expected InvalidHandleType"),
}
}
Comment on lines +196 to +208
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add a unit test for unrecognized numeric handle types.

The permissive Err(_) => Ok(()) branch in validate_handle_type is currently untested. Add a case using a raw non-enum integer (e.g., i32::MAX) so this compatibility contract doesn’t regress.

💡 Suggested test addition
 #[test]
 fn handle_type_unknown_is_allowed() {
@@
 }

+#[test]
+fn handle_type_unrecognized_numeric_is_allowed() {
+    let table = HandleTable::new();
+    let key = table.insert(HandleTableValue::FunctionRef { global_index: 4 });
+    let inbound = InboundValue {
+        value: Some(InboundValueVariant::Handle(BamlHandle {
+            key,
+            handle_type: i32::MAX,
+        })),
+    };
+    let out = inbound_to_external(inbound, &table).unwrap();
+    assert!(matches!(
+        out,
+        BexExternalValue::FunctionRef { global_index: 4 }
+    ));
+}

As per coding guidelines: "Prefer writing Rust unit tests over integration tests where possible."

}
Loading