use crate::error::{NeuralError, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use super::types::TargetPlatform;
pub(super) fn ensure_host_platform(platform: &TargetPlatform) -> Result<()> {
match TargetPlatform::host() {
Some(host) if host == *platform => Ok(()),
Some(host) => Err(NeuralError::InvalidArgument(format!(
"host code generation was requested for platform {platform:?}, but this machine's \
host platform is {host:?}; cross-compilation is not supported by ModelPackager's \
code generation pipeline"
))),
None => Err(NeuralError::DeviceNotFound(format!(
"this machine's OS/architecture combination ({}/{}) is not a supported host target \
for ModelPackager's code generation pipeline",
std::env::consts::OS,
std::env::consts::ARCH
))),
}
}
pub(super) fn unique_temp_dir(prefix: &str) -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let pid = std::process::id();
std::env::temp_dir().join(format!("{prefix}_{pid}_{nanos}_{count}"))
}
pub(super) fn cargo_build_release(manifest_path: &Path) -> Result<()> {
let output = Command::new("cargo")
.arg("build")
.arg("--release")
.arg("--manifest-path")
.arg(manifest_path)
.output()
.map_err(|e| NeuralError::IOError(format!("failed to invoke `cargo build`: {e}")))?;
if !output.status.success() {
return Err(
NeuralError::ComputationError(
format!(
"`cargo build --release` failed (exit status {:?}):\n--- stdout ---\n{}\n--- stderr ---\n{}",
output.status.code(), String::from_utf8_lossy(& output.stdout),
String::from_utf8_lossy(& output.stderr),
),
),
);
}
Ok(())
}
pub(super) fn binary_project_cargo_toml(project_name: &str, neural_manifest_dir: &str) -> String {
format!(
r#"[package]
name = "{project_name}"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "{project_name}"
path = "src/main.rs"
[dependencies]
scirs2-neural = {{ path = "{neural_manifest_dir}", features = ["legacy_serialization"] }}
[profile.release]
opt-level = 2
"#
)
}
pub(super) fn binary_project_main_rs() -> String {
r#"//! Auto-generated by `scirs2_neural::serving` host code generation.
//! Do not edit by hand — regenerate via `ModelPackager::generate_runtime_binary`.
use scirs2_core::ndarray::{Array, IxDyn};
use scirs2_neural::layers::{Dense, Layer};
use scirs2_neural::models::sequential::Sequential;
use scirs2_neural::models::Model;
use scirs2_neural::serialization::{load_model, SerializationFormat};
fn main() {
if let Err(e) = run() {
eprintln!("scirs2 model runtime error: {e}");
std::process::exit(1);
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let exe_path = std::env::current_exe()?;
let default_model_path = exe_path
.parent()
.map(|dir| dir.join("model.json"))
.ok_or("could not determine executable directory")?;
let model_path = std::env::args()
.nth(1)
.map(std::path::PathBuf::from)
.unwrap_or(default_model_path);
let model: Sequential<f32> = load_model(&model_path, SerializationFormat::JSON)?;
let input_dim = model
.layers()
.first()
.and_then(|layer| layer.as_any().downcast_ref::<Dense<f32>>())
.map(|dense| dense.input_dim())
.ok_or("packaged model has no Dense input layer")?;
let cli_values: Vec<f32> = std::env::args()
.skip(2)
.filter_map(|arg| arg.parse::<f32>().ok())
.collect();
let input_values = if cli_values.len() == input_dim {
cli_values
} else {
vec![1.0_f32; input_dim]
};
let input = Array::from_shape_vec(IxDyn(&[1, input_dim]), input_values)?;
let output = model.forward(&input)?;
println!("{output:?}");
Ok(())
}
"#
.to_string()
}
pub(super) fn cdylib_project_cargo_toml(project_name: &str, neural_manifest_dir: &str) -> String {
format!(
r#"[package]
name = "{project_name}"
version = "0.1.0"
edition = "2021"
[lib]
name = "{project_name}"
crate-type = ["cdylib"]
path = "src/lib.rs"
[dependencies]
scirs2-neural = {{ path = "{neural_manifest_dir}", features = ["legacy_serialization"] }}
[profile.release]
opt-level = 2
"#
)
}
pub(super) fn cdylib_project_lib_rs() -> String {
r#"//! Auto-generated by `scirs2_neural::serving` host code generation.
//! Implements the C ABI declared in the accompanying `scirs2_model.h` header.
//! Do not edit by hand — regenerate via `ModelPackager::generate_shared_library`.
use scirs2_core::ndarray::{Array, IxDyn};
use scirs2_neural::models::sequential::Sequential;
use scirs2_neural::models::Model;
use scirs2_neural::serialization::{load_model, SerializationFormat};
use std::ffi::{c_char, c_void, CStr};
use std::slice;
#[repr(C)]
pub struct ScirsTensor {
pub data: *mut c_void,
pub size: usize,
pub shape: *mut usize,
pub ndim: usize,
}
#[repr(C)]
pub struct ScirsModel {
pub handle: *mut c_void,
}
struct ModelHandle {
model: Sequential<f32>,
}
/// # Safety
/// `model_path` must be a valid, NUL-terminated C string, and `model` must
/// point to valid, writable memory for a [`ScirsModel`].
#[no_mangle]
pub unsafe extern "C" fn scirs2_model_load(
model_path: *const c_char,
model: *mut ScirsModel,
) -> i32 {
if model_path.is_null() || model.is_null() {
return -1;
}
let path_str = match CStr::from_ptr(model_path).to_str() {
Ok(s) => s,
Err(_) => return -1,
};
match load_model::<f32, _>(path_str, SerializationFormat::JSON) {
Ok(seq) => {
let handle = Box::new(ModelHandle { model: seq });
(*model).handle = Box::into_raw(handle) as *mut c_void;
0
}
Err(_) => -1,
}
}
/// # Safety
/// `model` and `input` must be valid pointers produced by this library (`model`
/// via `scirs2_model_load`); `output` must point to writable memory. On success,
/// `output.data` and `output.shape` are heap-allocated by this function and must
/// later be released via `scirs2_tensor_free`.
#[no_mangle]
pub unsafe extern "C" fn scirs2_model_predict(
model: *mut ScirsModel,
input: *const ScirsTensor,
output: *mut ScirsTensor,
) -> i32 {
if model.is_null() || input.is_null() || output.is_null() {
return -1;
}
let handle = &*((*model).handle as *const ModelHandle);
let in_tensor = &*input;
if in_tensor.data.is_null() || in_tensor.shape.is_null() {
return -1;
}
let shape: Vec<usize> = slice::from_raw_parts(in_tensor.shape, in_tensor.ndim).to_vec();
let data: Vec<f32> = slice::from_raw_parts(in_tensor.data as *const f32, in_tensor.size).to_vec();
let input_array = match Array::from_shape_vec(IxDyn(&shape), data) {
Ok(arr) => arr,
Err(_) => return -1,
};
match handle.model.forward(&input_array) {
Ok(out_array) => {
let out_shape = out_array.shape().to_vec();
let out_data: Vec<f32> = out_array.iter().copied().collect();
let mut data_box = out_data.into_boxed_slice();
let data_ptr = data_box.as_mut_ptr() as *mut c_void;
let size = data_box.len();
std::mem::forget(data_box);
let mut shape_box = out_shape.into_boxed_slice();
let shape_ptr = shape_box.as_mut_ptr();
let ndim = shape_box.len();
std::mem::forget(shape_box);
(*output).data = data_ptr;
(*output).size = size;
(*output).shape = shape_ptr;
(*output).ndim = ndim;
0
}
Err(_) => -1,
}
}
/// # Safety
/// `model` must be a pointer previously produced by `scirs2_model_load`, or null.
#[no_mangle]
pub unsafe extern "C" fn scirs2_model_free(model: *mut ScirsModel) {
if model.is_null() {
return;
}
if !(*model).handle.is_null() {
drop(Box::from_raw((*model).handle as *mut ModelHandle));
(*model).handle = std::ptr::null_mut();
}
}
/// # Safety
/// `tensor` must be a pointer previously populated by `scirs2_model_predict`, or null.
#[no_mangle]
pub unsafe extern "C" fn scirs2_tensor_free(tensor: *mut ScirsTensor) {
if tensor.is_null() {
return;
}
let t = &mut *tensor;
if !t.data.is_null() {
drop(Box::from_raw(slice::from_raw_parts_mut(
t.data as *mut f32,
t.size,
)));
t.data = std::ptr::null_mut();
}
if !t.shape.is_null() {
drop(Box::from_raw(slice::from_raw_parts_mut(t.shape, t.ndim)));
t.shape = std::ptr::null_mut();
}
}
"#
.to_string()
}