scirs2-neural 0.6.4

Neural network building blocks module for SciRS2 (scirs2-neural) - Minimal Version
Documentation
//! Real host-target code generation helpers backing [`super::packager::ModelPackager`].
//!
//! This module owns the mechanics behind `generate_runtime_binary` and
//! `generate_shared_library`: detecting the host platform, scaffolding unique
//! temp-directory Cargo projects, shelling out to `cargo build --release`, and
//! rendering the `Cargo.toml`/`main.rs`/`lib.rs` templates for those projects.

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;

/// Ensure `platform` matches the host this process is executing on, returning a
/// descriptive error otherwise. `ModelPackager`'s code generation pipeline only
/// supports building for the current host; it never fabricates a cross-compiled
/// artifact.
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
        ))),
    }
}
/// Build a unique, process- and call-safe temporary directory path under
/// [`std::env::temp_dir`]. The directory is *not* created by this function.
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}"))
}
/// Shell out to `cargo build --release` for the given manifest, mapping any
/// failure (missing `cargo`, or a failed compile) to a [`NeuralError`] that embeds
/// the captured stderr for debuggability.
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(())
}
/// Generate the `Cargo.toml` for the scaffolded runtime-binary project.
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
"#
    )
}
/// Generate the `src/main.rs` for the scaffolded runtime-binary project.
///
/// The emitted program loads a `model.json` (from argv\[1\], defaulting to a file
/// named `model.json` next to the executable), runs a single forward pass, and
/// prints the result — a genuine, standalone consumer of the packaged model.
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()
}
/// Generate the `Cargo.toml` for the scaffolded `cdylib` project.
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
"#
    )
}
/// Generate the `src/lib.rs` for the scaffolded `cdylib` project, implementing
/// the exact C ABI declared by [`ModelPackager::generate_c_header`]:
/// `scirs2_model_load`, `scirs2_model_predict`, `scirs2_model_free`, and
/// `scirs2_tensor_free`.
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()
}