#![cfg(feature = "async")]
use std::collections::BTreeMap;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::sync::Arc;
use tokio::sync::Mutex;
use tensogram::types::DataObjectDescriptor;
use tensogram::{AsyncStreamingEncoder, EncodeOptions};
use crate::async_core::{TaskResult, TgmAsyncTask, TgmCancellationToken, spawn_or_set_error};
use crate::{TgmError, set_last_error};
pub struct TgmAsyncStreamingEncoder {
inner: Arc<Mutex<Option<AsyncStreamingEncoder<tokio::fs::File>>>>,
path_string: CString,
}
#[unsafe(no_mangle)]
pub extern "C" fn tgm_async_streaming_encoder_path(
enc: *const TgmAsyncStreamingEncoder,
) -> *const c_char {
if enc.is_null() {
return std::ptr::null();
}
unsafe { (*enc).path_string.as_ptr() }
}
#[unsafe(no_mangle)]
pub extern "C" fn tgm_async_streaming_encoder_free(enc: *mut TgmAsyncStreamingEncoder) {
if !enc.is_null() {
unsafe { drop(Box::from_raw(enc)) };
}
}
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub extern "C" fn tgm_async_streaming_encoder_create(
path: *const c_char,
metadata_json: *const c_char,
hash_algo: *const c_char,
threads: u32,
cancel: *mut TgmCancellationToken,
timeout_ms: u64,
out_task: *mut *mut TgmAsyncTask,
) -> TgmError {
if path.is_null() || metadata_json.is_null() || out_task.is_null() {
set_last_error("null argument");
return TgmError::InvalidArg;
}
let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
Ok(s) => s.to_string(),
Err(e) => {
set_last_error(&format!("invalid UTF-8 in path: {e}"));
return TgmError::InvalidArg;
}
};
let json_str = match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
Ok(s) => s.to_string(),
Err(e) => {
set_last_error(&format!("invalid UTF-8 in metadata_json: {e}"));
return TgmError::InvalidArg;
}
};
let global_metadata = match crate::parse_streaming_metadata_json(&json_str) {
Ok(m) => m,
Err(e) => {
set_last_error(&e);
return TgmError::Metadata;
}
};
let hashing = match crate::parse_hash_algo(hash_algo) {
Ok(b) => b,
Err((code, msg)) => {
set_last_error(&msg);
return code;
}
};
let cancel_ref = if cancel.is_null() {
None
} else {
Some(unsafe { &*cancel })
};
let opts = EncodeOptions {
hashing,
threads,
..Default::default()
};
let path_for_task = path_str.clone();
let fut = async move {
let file = tokio::fs::File::create(&path_for_task)
.await
.map_err(tensogram::TensogramError::Io)?;
let enc = AsyncStreamingEncoder::new(file, &global_metadata, &opts).await?;
let path_string = CString::new(path_for_task.as_str()).unwrap_or_default();
let handle = Box::new(TgmAsyncStreamingEncoder {
inner: Arc::new(Mutex::new(Some(enc))),
path_string,
});
Ok(TaskResult::AsyncStreamingEncoder(handle))
};
spawn_or_set_error(fut, cancel_ref, timeout_ms, out_task)
}
#[derive(Clone, Copy)]
enum WriteKind {
Object,
PreEncoded,
}
#[allow(clippy::too_many_arguments)]
fn write_object_dispatch(
enc: *mut TgmAsyncStreamingEncoder,
descriptor_json: *const c_char,
data: *const u8,
len: usize,
cancel: *mut TgmCancellationToken,
timeout_ms: u64,
out_task: *mut *mut TgmAsyncTask,
kind: WriteKind,
) -> TgmError {
if enc.is_null() || descriptor_json.is_null() || data.is_null() || out_task.is_null() {
set_last_error("null argument");
return TgmError::InvalidArg;
}
let inner = unsafe { (*enc).inner.clone() };
let json_str = match unsafe { CStr::from_ptr(descriptor_json) }.to_str() {
Ok(s) => s.to_string(),
Err(e) => {
set_last_error(&format!("invalid UTF-8 in descriptor_json: {e}"));
return TgmError::InvalidArg;
}
};
let descriptor: DataObjectDescriptor = match serde_json::from_str(&json_str) {
Ok(d) => d,
Err(e) => {
set_last_error(&format!("invalid descriptor JSON: {e}"));
return TgmError::Metadata;
}
};
let data_vec = unsafe { std::slice::from_raw_parts(data, len) }.to_vec();
let cancel_ref = if cancel.is_null() {
None
} else {
Some(unsafe { &*cancel })
};
let fut = async move {
let mut guard = inner.lock().await;
let enc = guard.as_mut().ok_or_else(|| {
tensogram::TensogramError::Framing("encoder already finished".to_string())
})?;
match kind {
WriteKind::Object => enc.write_object(&descriptor, &data_vec).await?,
WriteKind::PreEncoded => enc.write_object_pre_encoded(&descriptor, &data_vec).await?,
}
Ok(TaskResult::Void)
};
spawn_or_set_error(fut, cancel_ref, timeout_ms, out_task)
}
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub extern "C" fn tgm_async_streaming_encoder_write_object(
enc: *mut TgmAsyncStreamingEncoder,
descriptor_json: *const c_char,
data: *const u8,
len: usize,
cancel: *mut TgmCancellationToken,
timeout_ms: u64,
out_task: *mut *mut TgmAsyncTask,
) -> TgmError {
write_object_dispatch(
enc,
descriptor_json,
data,
len,
cancel,
timeout_ms,
out_task,
WriteKind::Object,
)
}
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub extern "C" fn tgm_async_streaming_encoder_write_pre_encoded(
enc: *mut TgmAsyncStreamingEncoder,
descriptor_json: *const c_char,
data: *const u8,
len: usize,
cancel: *mut TgmCancellationToken,
timeout_ms: u64,
out_task: *mut *mut TgmAsyncTask,
) -> TgmError {
write_object_dispatch(
enc,
descriptor_json,
data,
len,
cancel,
timeout_ms,
out_task,
WriteKind::PreEncoded,
)
}
#[unsafe(no_mangle)]
pub extern "C" fn tgm_async_streaming_encoder_write_preceder(
enc: *mut TgmAsyncStreamingEncoder,
metadata_json: *const c_char,
cancel: *mut TgmCancellationToken,
timeout_ms: u64,
out_task: *mut *mut TgmAsyncTask,
) -> TgmError {
if enc.is_null() || metadata_json.is_null() || out_task.is_null() {
set_last_error("null argument");
return TgmError::InvalidArg;
}
let inner = unsafe { (*enc).inner.clone() };
let json_str = match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
Ok(s) => s.to_string(),
Err(e) => {
set_last_error(&format!("invalid UTF-8 in metadata_json: {e}"));
return TgmError::InvalidArg;
}
};
let value: serde_json::Value = match serde_json::from_str(&json_str) {
Ok(v) => v,
Err(e) => {
set_last_error(&format!("invalid metadata JSON: {e}"));
return TgmError::Metadata;
}
};
let map = match preceder_json_to_cbor_map(value) {
Ok(m) => m,
Err(e) => {
set_last_error(&e);
return TgmError::Metadata;
}
};
let cancel_ref = if cancel.is_null() {
None
} else {
Some(unsafe { &*cancel })
};
let fut = async move {
let mut guard = inner.lock().await;
let enc = guard.as_mut().ok_or_else(|| {
tensogram::TensogramError::Framing("encoder already finished".to_string())
})?;
enc.write_preceder(map).await?;
Ok(TaskResult::Void)
};
spawn_or_set_error(fut, cancel_ref, timeout_ms, out_task)
}
#[unsafe(no_mangle)]
pub extern "C" fn tgm_async_streaming_encoder_finish(
enc: *mut TgmAsyncStreamingEncoder,
backfill: bool,
cancel: *mut TgmCancellationToken,
timeout_ms: u64,
out_task: *mut *mut TgmAsyncTask,
) -> TgmError {
if enc.is_null() || out_task.is_null() {
set_last_error("null argument");
return TgmError::InvalidArg;
}
let inner = unsafe { (*enc).inner.clone() };
let cancel_ref = if cancel.is_null() {
None
} else {
Some(unsafe { &*cancel })
};
let fut = async move {
let mut guard = inner.lock().await;
let enc = guard.take().ok_or_else(|| {
tensogram::TensogramError::Framing("encoder already finished".to_string())
})?;
if backfill {
let _file = enc.finish_with_backfill().await?;
} else {
let _file = enc.finish().await?;
}
Ok(TaskResult::Void)
};
spawn_or_set_error(fut, cancel_ref, timeout_ms, out_task)
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TgmObjectCountStatus {
Ok = 0,
NullHandle = 1,
Busy = 2,
Finished = 3,
}
#[unsafe(no_mangle)]
pub extern "C" fn tgm_async_streaming_encoder_try_object_count(
enc: *const TgmAsyncStreamingEncoder,
out_count: *mut usize,
) -> TgmObjectCountStatus {
if enc.is_null() || out_count.is_null() {
return TgmObjectCountStatus::NullHandle;
}
let inner = unsafe { &*enc }.inner.clone();
match inner.try_lock() {
Ok(g) => match g.as_ref() {
Some(e) => {
unsafe { *out_count = e.object_count() };
TgmObjectCountStatus::Ok
}
None => TgmObjectCountStatus::Finished,
},
Err(_) => TgmObjectCountStatus::Busy,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn tgm_async_streaming_encoder_object_count(
enc: *const TgmAsyncStreamingEncoder,
) -> usize {
let mut count: usize = 0;
match tgm_async_streaming_encoder_try_object_count(enc, &mut count) {
TgmObjectCountStatus::Ok => count,
_ => usize::MAX,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn tgm_async_task_join_async_streaming_encoder(
task: *mut TgmAsyncTask,
out: *mut *mut TgmAsyncStreamingEncoder,
) -> TgmError {
if out.is_null() {
set_last_error("null out pointer");
return TgmError::InvalidArg;
}
match crate::async_core::join_internal(task) {
Ok(TaskResult::AsyncStreamingEncoder(h)) => {
unsafe { *out = Box::into_raw(h) };
TgmError::Ok
}
Ok(_) => {
set_last_error("task result type mismatch (expected async_streaming_encoder)");
TgmError::InvalidArg
}
Err(code) => code,
}
}
fn preceder_json_to_cbor_map(
v: serde_json::Value,
) -> Result<BTreeMap<String, ciborium::Value>, String> {
let obj = match v {
serde_json::Value::Object(m) => m,
_ => return Err("preceder metadata must be a JSON object".to_string()),
};
let mut out = BTreeMap::new();
for (k, val) in obj {
out.insert(k, json_to_cbor(val));
}
Ok(out)
}
fn json_to_cbor(v: serde_json::Value) -> ciborium::Value {
use ciborium::value::Integer;
match v {
serde_json::Value::Null => ciborium::Value::Null,
serde_json::Value::Bool(b) => ciborium::Value::Bool(b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
ciborium::Value::Integer(Integer::from(i))
} else if let Some(u) = n.as_u64() {
ciborium::Value::Integer(Integer::from(u))
} else if let Some(f) = n.as_f64() {
ciborium::Value::Float(f)
} else {
ciborium::Value::Null
}
}
serde_json::Value::String(s) => ciborium::Value::Text(s),
serde_json::Value::Array(a) => {
ciborium::Value::Array(a.into_iter().map(json_to_cbor).collect())
}
serde_json::Value::Object(m) => ciborium::Value::Map(
m.into_iter()
.map(|(k, v)| (ciborium::Value::Text(k), json_to_cbor(v)))
.collect(),
),
}
}