diff --git a/src/engine2/src/internal/model.ts b/src/engine2/src/internal/model.ts index 72191f8e..24b4c1b1 100644 --- a/src/engine2/src/internal/model.ts +++ b/src/engine2/src/internal/model.ts @@ -14,6 +14,7 @@ import { readOutPtr, allocOutUsize, readOutUsize, + copyFromWasm, } from './memory'; import { SimlinModelPtr, SimlinLinksPtr } from './types'; import { @@ -282,3 +283,129 @@ export function simlin_model_get_incoming_links(model: SimlinModelPtr, varName: free(outErrPtr); } } + +/** + * Get all stocks in a model as JSON bytes. + * @param model Model pointer + * @returns JSON bytes (UTF-8 encoded array of stock objects) + */ +export function simlin_model_get_stocks_json(model: SimlinModelPtr): Uint8Array { + const exports = getExports(); + const fn = exports.simlin_model_get_stocks_json as ( + model: number, + outBuf: number, + outLen: number, + outErr: number, + ) => void; + + const outBufPtr = allocOutPtr(); + const outLenPtr = allocOutUsize(); + const outErrPtr = allocOutPtr(); + + try { + fn(model, outBufPtr, outLenPtr, outErrPtr); + const errPtr = readOutPtr(outErrPtr); + + if (errPtr !== 0) { + const code = simlin_error_get_code(errPtr); + const message = simlin_error_get_message(errPtr) ?? 'Unknown error'; + const details = readAllErrorDetails(errPtr); + simlin_error_free(errPtr); + throw new SimlinError(message, code, details); + } + + const bufPtr = readOutPtr(outBufPtr); + const len = readOutUsize(outLenPtr); + const data = copyFromWasm(bufPtr, len); + free(bufPtr); + return data; + } finally { + free(outBufPtr); + free(outLenPtr); + free(outErrPtr); + } +} + +/** + * Get all flows in a model as JSON bytes. + * @param model Model pointer + * @returns JSON bytes (UTF-8 encoded array of flow objects) + */ +export function simlin_model_get_flows_json(model: SimlinModelPtr): Uint8Array { + const exports = getExports(); + const fn = exports.simlin_model_get_flows_json as ( + model: number, + outBuf: number, + outLen: number, + outErr: number, + ) => void; + + const outBufPtr = allocOutPtr(); + const outLenPtr = allocOutUsize(); + const outErrPtr = allocOutPtr(); + + try { + fn(model, outBufPtr, outLenPtr, outErrPtr); + const errPtr = readOutPtr(outErrPtr); + + if (errPtr !== 0) { + const code = simlin_error_get_code(errPtr); + const message = simlin_error_get_message(errPtr) ?? 'Unknown error'; + const details = readAllErrorDetails(errPtr); + simlin_error_free(errPtr); + throw new SimlinError(message, code, details); + } + + const bufPtr = readOutPtr(outBufPtr); + const len = readOutUsize(outLenPtr); + const data = copyFromWasm(bufPtr, len); + free(bufPtr); + return data; + } finally { + free(outBufPtr); + free(outLenPtr); + free(outErrPtr); + } +} + +/** + * Get all auxiliaries in a model as JSON bytes. + * @param model Model pointer + * @returns JSON bytes (UTF-8 encoded array of auxiliary objects) + */ +export function simlin_model_get_auxs_json(model: SimlinModelPtr): Uint8Array { + const exports = getExports(); + const fn = exports.simlin_model_get_auxs_json as ( + model: number, + outBuf: number, + outLen: number, + outErr: number, + ) => void; + + const outBufPtr = allocOutPtr(); + const outLenPtr = allocOutUsize(); + const outErrPtr = allocOutPtr(); + + try { + fn(model, outBufPtr, outLenPtr, outErrPtr); + const errPtr = readOutPtr(outErrPtr); + + if (errPtr !== 0) { + const code = simlin_error_get_code(errPtr); + const message = simlin_error_get_message(errPtr) ?? 'Unknown error'; + const details = readAllErrorDetails(errPtr); + simlin_error_free(errPtr); + throw new SimlinError(message, code, details); + } + + const bufPtr = readOutPtr(outBufPtr); + const len = readOutUsize(outLenPtr); + const data = copyFromWasm(bufPtr, len); + free(bufPtr); + return data; + } finally { + free(outBufPtr); + free(outLenPtr); + free(outErrPtr); + } +} diff --git a/src/engine2/src/model.ts b/src/engine2/src/model.ts index 641c8ba6..95c536ac 100644 --- a/src/engine2/src/model.ts +++ b/src/engine2/src/model.ts @@ -15,6 +15,9 @@ import { simlin_model_get_incoming_links, simlin_model_get_links, simlin_model_get_latex_equation, + simlin_model_get_stocks_json, + simlin_model_get_flows_json, + simlin_model_get_auxs_json, } from './internal/model'; import { readLinks, simlin_free_links } from './internal/analysis'; import { SimlinModelPtr, SimlinLinkPolarity, Link as LowLevelLink } from './internal/types'; @@ -215,8 +218,12 @@ export class Model { return this._cachedStocks; } - const model = this.getModelJson(); - this._cachedStocks = (model.stocks || []).map((s: JsonStock) => ({ + // Use direct JSON API instead of serializing entire project + const jsonBytes = simlin_model_get_stocks_json(this._ptr); + const jsonStr = new TextDecoder().decode(jsonBytes); + const stocksJson: JsonStock[] = JSON.parse(jsonStr); + + this._cachedStocks = stocksJson.map((s: JsonStock) => ({ type: 'stock' as const, name: s.name, initialEquation: this.extractEquation(s.initialEquation, s.arrayedEquation, 'initialEquation'), @@ -240,8 +247,12 @@ export class Model { return this._cachedFlows; } - const model = this.getModelJson(); - this._cachedFlows = (model.flows || []).map((f: JsonFlow) => { + // Use direct JSON API instead of serializing entire project + const jsonBytes = simlin_model_get_flows_json(this._ptr); + const jsonStr = new TextDecoder().decode(jsonBytes); + const flowsJson: JsonFlow[] = JSON.parse(jsonStr); + + this._cachedFlows = flowsJson.map((f: JsonFlow) => { let gf: GraphicalFunction | undefined; if (f.graphicalFunction) { gf = this.parseJsonGraphicalFunction(f.graphicalFunction); @@ -271,8 +282,12 @@ export class Model { return this._cachedAuxs; } - const model = this.getModelJson(); - this._cachedAuxs = (model.auxiliaries || []).map((a: JsonAuxiliary) => { + // Use direct JSON API instead of serializing entire project + const jsonBytes = simlin_model_get_auxs_json(this._ptr); + const jsonStr = new TextDecoder().decode(jsonBytes); + const auxsJson: JsonAuxiliary[] = JSON.parse(jsonStr); + + this._cachedAuxs = auxsJson.map((a: JsonAuxiliary) => { let gf: GraphicalFunction | undefined; if (a.graphicalFunction) { gf = this.parseJsonGraphicalFunction(a.graphicalFunction); diff --git a/src/libsimlin/simlin.h b/src/libsimlin/simlin.h index 01184b0f..a9740514 100644 --- a/src/libsimlin/simlin.h +++ b/src/libsimlin/simlin.h @@ -388,6 +388,63 @@ char *simlin_model_get_latex_equation(SimlinModel *model, const char *ident, SimlinError **out_error); +// Returns all stocks in a model as a JSON array. +// +// The returned JSON is an array of stock objects, each containing fields like +// `name`, `initialEquation`, `inflows`, `outflows`, `units`, etc. +// +// # Safety +// - `model` must be a valid pointer to a SimlinModel +// - `out_buffer` and `out_len` must be valid pointers where the serialized +// bytes and length will be written +// - `out_error` may be null or a valid pointer to receive error details +// +// # Ownership +// - The returned buffer is exclusively owned by the caller and MUST be freed with `simlin_free`. +// - The caller is responsible for freeing the buffer even if subsequent operations fail. +void simlin_model_get_stocks_json(SimlinModel *model, + uint8_t **out_buffer, + uintptr_t *out_len, + SimlinError **out_error); + +// Returns all flows in a model as a JSON array. +// +// The returned JSON is an array of flow objects, each containing fields like +// `name`, `equation`, `units`, `nonNegative`, `graphicalFunction`, etc. +// +// # Safety +// - `model` must be a valid pointer to a SimlinModel +// - `out_buffer` and `out_len` must be valid pointers where the serialized +// bytes and length will be written +// - `out_error` may be null or a valid pointer to receive error details +// +// # Ownership +// - The returned buffer is exclusively owned by the caller and MUST be freed with `simlin_free`. +// - The caller is responsible for freeing the buffer even if subsequent operations fail. +void simlin_model_get_flows_json(SimlinModel *model, + uint8_t **out_buffer, + uintptr_t *out_len, + SimlinError **out_error); + +// Returns all auxiliaries in a model as a JSON array. +// +// The returned JSON is an array of auxiliary objects, each containing fields like +// `name`, `equation`, `initialEquation`, `units`, `graphicalFunction`, etc. +// +// # Safety +// - `model` must be a valid pointer to a SimlinModel +// - `out_buffer` and `out_len` must be valid pointers where the serialized +// bytes and length will be written +// - `out_error` may be null or a valid pointer to receive error details +// +// # Ownership +// - The returned buffer is exclusively owned by the caller and MUST be freed with `simlin_free`. +// - The caller is responsible for freeing the buffer even if subsequent operations fail. +void simlin_model_get_auxs_json(SimlinModel *model, + uint8_t **out_buffer, + uintptr_t *out_len, + SimlinError **out_error); + // Creates a new simulation context // // # Safety diff --git a/src/libsimlin/src/lib.rs b/src/libsimlin/src/lib.rs index 180c5cad..072ae162 100644 --- a/src/libsimlin/src/lib.rs +++ b/src/libsimlin/src/lib.rs @@ -1434,6 +1434,295 @@ pub unsafe extern "C" fn simlin_model_get_latex_equation( } } +/// Returns all stocks in a model as a JSON array. +/// +/// The returned JSON is an array of stock objects, each containing fields like +/// `name`, `initialEquation`, `inflows`, `outflows`, `units`, etc. +/// +/// # Safety +/// - `model` must be a valid pointer to a SimlinModel +/// - `out_buffer` and `out_len` must be valid pointers where the serialized +/// bytes and length will be written +/// - `out_error` may be null or a valid pointer to receive error details +/// +/// # Ownership +/// - The returned buffer is exclusively owned by the caller and MUST be freed with `simlin_free`. +/// - The caller is responsible for freeing the buffer even if subsequent operations fail. +#[no_mangle] +pub unsafe extern "C" fn simlin_model_get_stocks_json( + model: *mut SimlinModel, + out_buffer: *mut *mut u8, + out_len: *mut usize, + out_error: *mut *mut SimlinError, +) { + clear_out_error(out_error); + if out_buffer.is_null() || out_len.is_null() { + store_error( + out_error, + SimlinError::new(SimlinErrorCode::Generic) + .with_message("output pointers must not be NULL"), + ); + return; + } + + *out_buffer = ptr::null_mut(); + *out_len = 0; + + let model_ref = match require_model(model) { + Ok(m) => m, + Err(err) => { + store_anyhow_error(out_error, err); + return; + } + }; + + let project_locked = (*model_ref.project).project.lock().unwrap(); + + let datamodel_model = match project_locked.datamodel.get_model(&model_ref.model_name) { + Some(m) => m, + None => { + store_error( + out_error, + SimlinError::new(SimlinErrorCode::BadModelName) + .with_message(format!("model '{}' not found", model_ref.model_name)), + ); + return; + } + }; + + // Extract stocks from the datamodel and convert to JSON types + let stocks: Vec = datamodel_model + .variables + .iter() + .filter_map(|var| match var { + engine::datamodel::Variable::Stock(stock) => { + Some(engine::json::Stock::from(stock.clone())) + } + _ => None, + }) + .collect(); + + let bytes = match serde_json::to_vec(&stocks) { + Ok(data) => data, + Err(err) => { + store_error( + out_error, + SimlinError::new(SimlinErrorCode::Generic) + .with_message(format!("failed to encode stocks JSON: {err}")), + ); + return; + } + }; + + let len = bytes.len(); + let buf = simlin_malloc(len); + if buf.is_null() { + store_error( + out_error, + SimlinError::new(SimlinErrorCode::Generic) + .with_message("allocation failed while serializing stocks"), + ); + return; + } + + std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, len); + + *out_buffer = buf; + *out_len = len; +} + +/// Returns all flows in a model as a JSON array. +/// +/// The returned JSON is an array of flow objects, each containing fields like +/// `name`, `equation`, `units`, `nonNegative`, `graphicalFunction`, etc. +/// +/// # Safety +/// - `model` must be a valid pointer to a SimlinModel +/// - `out_buffer` and `out_len` must be valid pointers where the serialized +/// bytes and length will be written +/// - `out_error` may be null or a valid pointer to receive error details +/// +/// # Ownership +/// - The returned buffer is exclusively owned by the caller and MUST be freed with `simlin_free`. +/// - The caller is responsible for freeing the buffer even if subsequent operations fail. +#[no_mangle] +pub unsafe extern "C" fn simlin_model_get_flows_json( + model: *mut SimlinModel, + out_buffer: *mut *mut u8, + out_len: *mut usize, + out_error: *mut *mut SimlinError, +) { + clear_out_error(out_error); + if out_buffer.is_null() || out_len.is_null() { + store_error( + out_error, + SimlinError::new(SimlinErrorCode::Generic) + .with_message("output pointers must not be NULL"), + ); + return; + } + + *out_buffer = ptr::null_mut(); + *out_len = 0; + + let model_ref = match require_model(model) { + Ok(m) => m, + Err(err) => { + store_anyhow_error(out_error, err); + return; + } + }; + + let project_locked = (*model_ref.project).project.lock().unwrap(); + + let datamodel_model = match project_locked.datamodel.get_model(&model_ref.model_name) { + Some(m) => m, + None => { + store_error( + out_error, + SimlinError::new(SimlinErrorCode::BadModelName) + .with_message(format!("model '{}' not found", model_ref.model_name)), + ); + return; + } + }; + + // Extract flows from the datamodel and convert to JSON types + let flows: Vec = datamodel_model + .variables + .iter() + .filter_map(|var| match var { + engine::datamodel::Variable::Flow(flow) => Some(engine::json::Flow::from(flow.clone())), + _ => None, + }) + .collect(); + + let bytes = match serde_json::to_vec(&flows) { + Ok(data) => data, + Err(err) => { + store_error( + out_error, + SimlinError::new(SimlinErrorCode::Generic) + .with_message(format!("failed to encode flows JSON: {err}")), + ); + return; + } + }; + + let len = bytes.len(); + let buf = simlin_malloc(len); + if buf.is_null() { + store_error( + out_error, + SimlinError::new(SimlinErrorCode::Generic) + .with_message("allocation failed while serializing flows"), + ); + return; + } + + std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, len); + + *out_buffer = buf; + *out_len = len; +} + +/// Returns all auxiliaries in a model as a JSON array. +/// +/// The returned JSON is an array of auxiliary objects, each containing fields like +/// `name`, `equation`, `initialEquation`, `units`, `graphicalFunction`, etc. +/// +/// # Safety +/// - `model` must be a valid pointer to a SimlinModel +/// - `out_buffer` and `out_len` must be valid pointers where the serialized +/// bytes and length will be written +/// - `out_error` may be null or a valid pointer to receive error details +/// +/// # Ownership +/// - The returned buffer is exclusively owned by the caller and MUST be freed with `simlin_free`. +/// - The caller is responsible for freeing the buffer even if subsequent operations fail. +#[no_mangle] +pub unsafe extern "C" fn simlin_model_get_auxs_json( + model: *mut SimlinModel, + out_buffer: *mut *mut u8, + out_len: *mut usize, + out_error: *mut *mut SimlinError, +) { + clear_out_error(out_error); + if out_buffer.is_null() || out_len.is_null() { + store_error( + out_error, + SimlinError::new(SimlinErrorCode::Generic) + .with_message("output pointers must not be NULL"), + ); + return; + } + + *out_buffer = ptr::null_mut(); + *out_len = 0; + + let model_ref = match require_model(model) { + Ok(m) => m, + Err(err) => { + store_anyhow_error(out_error, err); + return; + } + }; + + let project_locked = (*model_ref.project).project.lock().unwrap(); + + let datamodel_model = match project_locked.datamodel.get_model(&model_ref.model_name) { + Some(m) => m, + None => { + store_error( + out_error, + SimlinError::new(SimlinErrorCode::BadModelName) + .with_message(format!("model '{}' not found", model_ref.model_name)), + ); + return; + } + }; + + // Extract auxiliaries from the datamodel and convert to JSON types + let auxs: Vec = datamodel_model + .variables + .iter() + .filter_map(|var| match var { + engine::datamodel::Variable::Aux(aux) => { + Some(engine::json::Auxiliary::from(aux.clone())) + } + _ => None, + }) + .collect(); + + let bytes = match serde_json::to_vec(&auxs) { + Ok(data) => data, + Err(err) => { + store_error( + out_error, + SimlinError::new(SimlinErrorCode::Generic) + .with_message(format!("failed to encode auxiliaries JSON: {err}")), + ); + return; + } + }; + + let len = bytes.len(); + let buf = simlin_malloc(len); + if buf.is_null() { + store_error( + out_error, + SimlinError::new(SimlinErrorCode::Generic) + .with_message("allocation failed while serializing auxiliaries"), + ); + return; + } + + std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, len); + + *out_buffer = buf; + *out_len = len; +} + /// Helper function to create a VM for a given project and model fn create_vm(project: &engine::Project, model_name: &str) -> Result { let compiler = engine::Simulation::new(project, model_name)?; @@ -9742,4 +10031,231 @@ mod tests { simlin_project_unref(proj); } } + + #[test] + fn test_model_get_stocks_json() { + let datamodel = TestProject::new("stocks_json") + .stock("population", "100", &["births"], &["deaths"], None) + .stock("resources", "500", &[], &["consumption"], None) + .flow("births", "population * 0.02", None) + .flow("deaths", "population * 0.01", None) + .flow("consumption", "resources * 0.1", None) + .build_datamodel(); + let proj = open_project_from_datamodel(&datamodel); + + unsafe { + let mut err: *mut SimlinError = ptr::null_mut(); + let model = simlin_project_get_model(proj, ptr::null(), &mut err); + assert!(!model.is_null()); + assert!(err.is_null()); + + let mut out_buffer: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + let mut out_error: *mut SimlinError = ptr::null_mut(); + + simlin_model_get_stocks_json(model, &mut out_buffer, &mut out_len, &mut out_error); + + assert!(out_error.is_null(), "expected no error getting stocks JSON"); + assert!(!out_buffer.is_null(), "expected stocks JSON buffer"); + assert!(out_len > 0, "expected non-empty stocks JSON"); + + let slice = std::slice::from_raw_parts(out_buffer, out_len); + let json_str = std::str::from_utf8(slice).expect("valid utf-8 JSON"); + + let stocks: Vec = + serde_json::from_str(json_str).expect("valid JSON array of stocks"); + + assert_eq!(stocks.len(), 2, "expected 2 stocks"); + + let stock_names: Vec<&str> = stocks.iter().map(|s| s.name.as_str()).collect(); + assert!(stock_names.contains(&"population")); + assert!(stock_names.contains(&"resources")); + + // Verify stock details + let pop_stock = stocks.iter().find(|s| s.name == "population").unwrap(); + assert_eq!(pop_stock.initial_equation, "100"); + assert!(pop_stock.inflows.contains(&"births".to_string())); + assert!(pop_stock.outflows.contains(&"deaths".to_string())); + + simlin_free(out_buffer); + simlin_model_unref(model); + simlin_project_unref(proj); + } + } + + #[test] + fn test_model_get_flows_json() { + let datamodel = TestProject::new("flows_json") + .stock("population", "100", &["births"], &["deaths"], None) + .flow("births", "population * 0.02", None) + .flow("deaths", "population * 0.01", None) + .aux("growth_rate", "0.02", None) + .build_datamodel(); + let proj = open_project_from_datamodel(&datamodel); + + unsafe { + let mut err: *mut SimlinError = ptr::null_mut(); + let model = simlin_project_get_model(proj, ptr::null(), &mut err); + assert!(!model.is_null()); + assert!(err.is_null()); + + let mut out_buffer: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + let mut out_error: *mut SimlinError = ptr::null_mut(); + + simlin_model_get_flows_json(model, &mut out_buffer, &mut out_len, &mut out_error); + + assert!(out_error.is_null(), "expected no error getting flows JSON"); + assert!(!out_buffer.is_null(), "expected flows JSON buffer"); + assert!(out_len > 0, "expected non-empty flows JSON"); + + let slice = std::slice::from_raw_parts(out_buffer, out_len); + let json_str = std::str::from_utf8(slice).expect("valid utf-8 JSON"); + + let flows: Vec = + serde_json::from_str(json_str).expect("valid JSON array of flows"); + + assert_eq!(flows.len(), 2, "expected 2 flows"); + + let flow_names: Vec<&str> = flows.iter().map(|f| f.name.as_str()).collect(); + assert!(flow_names.contains(&"births")); + assert!(flow_names.contains(&"deaths")); + + // Verify flow details + let births_flow = flows.iter().find(|f| f.name == "births").unwrap(); + assert_eq!(births_flow.equation, "population * 0.02"); + + simlin_free(out_buffer); + simlin_model_unref(model); + simlin_project_unref(proj); + } + } + + #[test] + fn test_model_get_auxs_json() { + let datamodel = TestProject::new("auxs_json") + .stock("population", "100", &["births"], &[], None) + .flow("births", "population * growth_rate", None) + .aux("growth_rate", "0.02", None) + .aux("carrying_capacity", "1000", None) + .aux("density", "population / carrying_capacity", None) + .build_datamodel(); + let proj = open_project_from_datamodel(&datamodel); + + unsafe { + let mut err: *mut SimlinError = ptr::null_mut(); + let model = simlin_project_get_model(proj, ptr::null(), &mut err); + assert!(!model.is_null()); + assert!(err.is_null()); + + let mut out_buffer: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + let mut out_error: *mut SimlinError = ptr::null_mut(); + + simlin_model_get_auxs_json(model, &mut out_buffer, &mut out_len, &mut out_error); + + assert!(out_error.is_null(), "expected no error getting auxs JSON"); + assert!(!out_buffer.is_null(), "expected auxs JSON buffer"); + assert!(out_len > 0, "expected non-empty auxs JSON"); + + let slice = std::slice::from_raw_parts(out_buffer, out_len); + let json_str = std::str::from_utf8(slice).expect("valid utf-8 JSON"); + + let auxs: Vec = + serde_json::from_str(json_str).expect("valid JSON array of auxiliaries"); + + assert_eq!(auxs.len(), 3, "expected 3 auxiliaries"); + + let aux_names: Vec<&str> = auxs.iter().map(|a| a.name.as_str()).collect(); + assert!(aux_names.contains(&"growth_rate")); + assert!(aux_names.contains(&"carrying_capacity")); + assert!(aux_names.contains(&"density")); + + // Verify aux details + let growth_rate = auxs.iter().find(|a| a.name == "growth_rate").unwrap(); + assert_eq!(growth_rate.equation, "0.02"); + + simlin_free(out_buffer); + simlin_model_unref(model); + simlin_project_unref(proj); + } + } + + #[test] + fn test_model_get_stocks_json_empty() { + // Test model with no stocks + let datamodel = TestProject::new("no_stocks") + .aux("constant", "42", None) + .build_datamodel(); + let proj = open_project_from_datamodel(&datamodel); + + unsafe { + let mut err: *mut SimlinError = ptr::null_mut(); + let model = simlin_project_get_model(proj, ptr::null(), &mut err); + assert!(!model.is_null()); + + let mut out_buffer: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + let mut out_error: *mut SimlinError = ptr::null_mut(); + + simlin_model_get_stocks_json(model, &mut out_buffer, &mut out_len, &mut out_error); + + assert!(out_error.is_null()); + assert!(!out_buffer.is_null()); + + let slice = std::slice::from_raw_parts(out_buffer, out_len); + let json_str = std::str::from_utf8(slice).unwrap(); + + let stocks: Vec = serde_json::from_str(json_str).unwrap(); + assert!(stocks.is_empty(), "expected empty stocks array"); + + simlin_free(out_buffer); + simlin_model_unref(model); + simlin_project_unref(proj); + } + } + + #[test] + fn test_model_get_stocks_json_null_model() { + unsafe { + let mut out_buffer: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + let mut out_error: *mut SimlinError = ptr::null_mut(); + + simlin_model_get_stocks_json( + ptr::null_mut(), + &mut out_buffer, + &mut out_len, + &mut out_error, + ); + + assert!(!out_error.is_null(), "expected error for NULL model"); + assert_eq!(simlin_error_get_code(out_error), SimlinErrorCode::Generic); + simlin_error_free(out_error); + } + } + + #[test] + fn test_model_get_stocks_json_null_out_buffer() { + let datamodel = TestProject::new("null_buffer").build_datamodel(); + let proj = open_project_from_datamodel(&datamodel); + + unsafe { + let mut err: *mut SimlinError = ptr::null_mut(); + let model = simlin_project_get_model(proj, ptr::null(), &mut err); + assert!(!model.is_null()); + + let mut out_len: usize = 0; + let mut out_error: *mut SimlinError = ptr::null_mut(); + + simlin_model_get_stocks_json(model, ptr::null_mut(), &mut out_len, &mut out_error); + + assert!(!out_error.is_null(), "expected error for NULL out_buffer"); + assert_eq!(simlin_error_get_code(out_error), SimlinErrorCode::Generic); + simlin_error_free(out_error); + + simlin_model_unref(model); + simlin_project_unref(proj); + } + } } diff --git a/src/pysimlin/simlin/_ffi.py b/src/pysimlin/simlin/_ffi.py index 6d7e58f2..f4a793ed 100644 --- a/src/pysimlin/simlin/_ffi.py +++ b/src/pysimlin/simlin/_ffi.py @@ -246,6 +246,81 @@ def open_json(json_data: bytes) -> Any: return project_ptr +def get_stocks_json(model_ptr: Any) -> bytes: + """Get all stocks in a model as JSON. + + Args: + model_ptr: Pointer to a SimlinModel + + Returns: + JSON-encoded array of stock objects (UTF-8 bytes) + + Raises: + SimlinRuntimeError: If the operation fails + """ + output_ptr = ffi.new("uint8_t **") + output_len_ptr = ffi.new("uintptr_t *") + err_ptr = ffi.new("SimlinError **") + + lib.simlin_model_get_stocks_json(model_ptr, output_ptr, output_len_ptr, err_ptr) + check_out_error(err_ptr, "Get stocks JSON") + + try: + return bytes(ffi.buffer(output_ptr[0], output_len_ptr[0])) + finally: + lib.simlin_free(output_ptr[0]) + + +def get_flows_json(model_ptr: Any) -> bytes: + """Get all flows in a model as JSON. + + Args: + model_ptr: Pointer to a SimlinModel + + Returns: + JSON-encoded array of flow objects (UTF-8 bytes) + + Raises: + SimlinRuntimeError: If the operation fails + """ + output_ptr = ffi.new("uint8_t **") + output_len_ptr = ffi.new("uintptr_t *") + err_ptr = ffi.new("SimlinError **") + + lib.simlin_model_get_flows_json(model_ptr, output_ptr, output_len_ptr, err_ptr) + check_out_error(err_ptr, "Get flows JSON") + + try: + return bytes(ffi.buffer(output_ptr[0], output_len_ptr[0])) + finally: + lib.simlin_free(output_ptr[0]) + + +def get_auxs_json(model_ptr: Any) -> bytes: + """Get all auxiliaries in a model as JSON. + + Args: + model_ptr: Pointer to a SimlinModel + + Returns: + JSON-encoded array of auxiliary objects (UTF-8 bytes) + + Raises: + SimlinRuntimeError: If the operation fails + """ + output_ptr = ffi.new("uint8_t **") + output_len_ptr = ffi.new("uintptr_t *") + err_ptr = ffi.new("SimlinError **") + + lib.simlin_model_get_auxs_json(model_ptr, output_ptr, output_len_ptr, err_ptr) + check_out_error(err_ptr, "Get auxiliaries JSON") + + try: + return bytes(ffi.buffer(output_ptr[0], output_len_ptr[0])) + finally: + lib.simlin_free(output_ptr[0]) + + __all__ = [ "ffi", "lib", @@ -259,6 +334,9 @@ def open_json(json_data: bytes) -> Any: "apply_patch_json", "serialize_json", "open_json", + "get_stocks_json", + "get_flows_json", + "get_auxs_json", "_register_finalizer", "_finalizer_refs", ] diff --git a/src/pysimlin/simlin/_ffi_build.py b/src/pysimlin/simlin/_ffi_build.py index d88912af..8169b602 100644 --- a/src/pysimlin/simlin/_ffi_build.py +++ b/src/pysimlin/simlin/_ffi_build.py @@ -149,6 +149,9 @@ void simlin_model_get_var_names(SimlinModel *model, char **result, uintptr_t max, uintptr_t *out_written, OutError out_error); void simlin_model_get_incoming_links(SimlinModel *model, const char *var_name, char **result, uintptr_t max, uintptr_t *out_written, OutError out_error); SimlinLinks *simlin_model_get_links(SimlinModel *model, OutError out_error); +void simlin_model_get_stocks_json(SimlinModel *model, uint8_t **out_buffer, uintptr_t *out_len, OutError out_error); +void simlin_model_get_flows_json(SimlinModel *model, uint8_t **out_buffer, uintptr_t *out_len, OutError out_error); +void simlin_model_get_auxs_json(SimlinModel *model, uint8_t **out_buffer, uintptr_t *out_len, OutError out_error); SimlinSim *simlin_sim_new(SimlinModel *model, bool enable_ltm, OutError out_error); void simlin_sim_ref(SimlinSim *sim); void simlin_sim_unref(SimlinSim *sim); diff --git a/src/pysimlin/simlin/model.py b/src/pysimlin/simlin/model.py index 4e51ed16..c911ef3e 100644 --- a/src/pysimlin/simlin/model.py +++ b/src/pysimlin/simlin/model.py @@ -7,7 +7,7 @@ from types import TracebackType from ._dt import parse_dt -from ._ffi import ffi, lib, string_to_c, c_to_string, free_c_string, _register_finalizer, check_out_error +from ._ffi import ffi, lib, string_to_c, c_to_string, free_c_string, _register_finalizer, check_out_error, get_stocks_json, get_flows_json, get_auxs_json from .errors import SimlinRuntimeError, ErrorCode from .analysis import Link, LinkPolarity, Loop from .types import Stock, Flow, Aux, TimeSpec, GraphicalFunction, GraphicalFunctionScale, ModelIssue @@ -399,7 +399,10 @@ def stocks(self) -> tuple[Stock, ...]: Tuple of Stock objects representing all stocks in the model """ if self._cached_stocks is None: - model = self._get_model_json() + # Use direct JSON API instead of serializing entire project + json_bytes = get_stocks_json(self._ptr) + stocks_json = json.loads(json_bytes.decode("utf-8")) + stocks_list = [converter.structure(s, JsonStock) for s in stocks_json] self._cached_stocks = tuple( Stock( name=s.name, @@ -413,7 +416,7 @@ def stocks(self) -> tuple[Stock, ...]: dimensions=tuple(s.arrayed_equation.dimensions) if s.arrayed_equation else (), non_negative=s.non_negative, ) - for s in model.stocks + for s in stocks_list ) return self._cached_stocks @@ -426,10 +429,13 @@ def flows(self) -> tuple[Flow, ...]: Tuple of Flow objects representing all flows in the model """ if self._cached_flows is None: - model = self._get_model_json() + # Use direct JSON API instead of serializing entire project + json_bytes = get_flows_json(self._ptr) + flows_json = json.loads(json_bytes.decode("utf-8")) + flows_data = [converter.structure(f, JsonFlow) for f in flows_json] flows_list = [] - for f in model.flows: + for f in flows_data: gf = None if f.graphical_function: gf = self._parse_json_graphical_function(f.graphical_function) @@ -457,10 +463,13 @@ def auxs(self) -> tuple[Aux, ...]: Tuple of Aux objects representing all auxiliary variables in the model """ if self._cached_auxs is None: - model = self._get_model_json() + # Use direct JSON API instead of serializing entire project + json_bytes = get_auxs_json(self._ptr) + auxs_json = json.loads(json_bytes.decode("utf-8")) + auxs_data = [converter.structure(a, JsonAuxiliary) for a in auxs_json] auxs_list = [] - for a in model.auxiliaries: + for a in auxs_data: gf = None if a.graphical_function: gf = self._parse_json_graphical_function(a.graphical_function)