refactor(ffi): generate header via cbindgen
Adopt cbindgen in the build script to keep the C header aligned with the Rust FFI definitions. Store the patient handle as a void pointer to avoid layout mismatches and refresh the generated header and repository guidelines.
This commit is contained in:
@@ -1,41 +1,39 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- Source: `src/` (crate root `src/lib.rs`). Core modules in `src/organs/`, types in `src/types.rs`, errors in `src/error.rs`, FFI in `src/ffi.rs`.
|
||||
- Tests: integration tests in `tests/` (e.g., `tests/patient.rs`).
|
||||
- Examples: `examples/` (Rust usage and tracing demos), plus C FFI example in `examples/c/` with header in `ffi/medicallib.h`.
|
||||
- Benchmarks: `benches/` (Criterion-based; see `benches/heart.rs`).
|
||||
- Docs: `README.md`, `ARCHITECTURE.md`, `INSTALL.md`, `MIGRATION.md`.
|
||||
- `src/` contains the crate root (`src/lib.rs`) and organ modules under `src/organs/`.
|
||||
- Shared data types live in `src/types.rs`; domain errors in `src/error.rs`.
|
||||
- FFI layer is `src/ffi.rs` with matching C header in `ffi/medicallib.h`; keep them aligned.
|
||||
- Integration tests sit in `tests/` (run `cargo test --test patient` to target a file).
|
||||
- Usage and tracing demos live in `examples/`; the C example is in `examples/c/`.
|
||||
- Performance benchmarks use Criterion in `benches/` (see `benches/heart.rs`).
|
||||
- Architectural and setup docs are at the repo root (`README.md`, `ARCHITECTURE.md`, `INSTALL.md`, `MIGRATION.md`).
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- Build library (debug): `cargo build`
|
||||
- Build with FFI (shared lib): `cargo build --release --features ffi`
|
||||
- Run tests: `cargo test` (single test file: `cargo test --test patient`)
|
||||
- Run examples: `cargo run --example usage` (or `tracing_demo`)
|
||||
- Benchmarks (Criterion): `cargo bench`
|
||||
- Lint: `cargo clippy --all-targets -- -D warnings`
|
||||
- Format: `cargo fmt --all`
|
||||
- Docs (open): `cargo doc --no-deps --open`
|
||||
- `cargo build` compiles the library in debug mode for fast iteration.
|
||||
- `cargo build --release --features ffi` produces the shared library for FFI consumers.
|
||||
- `cargo test` runs the full test suite; append `--test <name>` for a single integration test.
|
||||
- `cargo run --example usage` or `cargo run --example tracing_demo` exercises examples.
|
||||
- `cargo bench` executes Criterion benchmarks; compare results across runs in `target/criterion/`.
|
||||
- `cargo clippy --all-targets -- -D warnings` enforces lint hygiene before committing.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Rust style via `rustfmt`; 4-space indentation; wrap at ~100 cols where reasonable.
|
||||
- Naming: modules/files `snake_case`, types/traits `CamelCase`, functions/vars `snake_case`, constants `SCREAMING_SNAKE_CASE`.
|
||||
- Public API changes must be intentional; update examples and docs when modifying `src/ffi.rs` or `src/organs/*`.
|
||||
- Use `cargo fmt --all` (4-space indent, ~100-char lines) before sending patches.
|
||||
- Modules and files use `snake_case`; types and traits use `CamelCase`; constants are `SCREAMING_SNAKE_CASE`.
|
||||
- Favor standard library and existing crates; document any `unsafe` with clear safety notes.
|
||||
- Keep public APIs cohesive within `src/organs/`; update examples/docs when the FFI surface changes.
|
||||
|
||||
## Testing Guidelines
|
||||
- Prefer integration tests in `tests/` named after features (e.g., `basic.rs`, `ffi.rs`).
|
||||
- Add unit tests near code under `#[cfg(test)]` for small invariants.
|
||||
- Keep tests deterministic; avoid sleeping/time unless necessary.
|
||||
- Run `cargo test` locally and ensure `cargo clippy` passes before opening a PR.
|
||||
- Prefer integration coverage in `tests/`; create files named after features (e.g., `tests/ffi.rs`).
|
||||
- Add focused unit tests near code under `#[cfg(test)]` for invariants.
|
||||
- Ensure tests are deterministic; avoid sleeps or external I/O without guards.
|
||||
- Run `cargo test` and `cargo clippy` locally before opening a PR.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Commits: clear, imperative subject (e.g., "Add heart rate update clamp"). Conventional Commits (feat/fix/refactor/docs/test) are welcome.
|
||||
- PRs: concise description, link issues, note breaking changes, and include tests or examples. Add screenshots only if relevant to docs.
|
||||
- Write imperative commit subjects (e.g., “Add heart rate update clamp”); Conventional Commits are welcome.
|
||||
- PRs should link related issues, call out breaking changes, and mention new tests or examples.
|
||||
- Include reproduction steps or screenshots only when they clarify user-facing changes.
|
||||
|
||||
## Security & FFI Notes
|
||||
- FFI: when changing `src/ffi.rs`, update C header `ffi/medicallib.h` and verify `examples/c/ffi_example.c` still builds against the produced `cdylib`.
|
||||
- Avoid `unsafe` unless essential; document safety contracts.
|
||||
|
||||
## Agent-Specific Tips
|
||||
- Maintain module organization under `src/organs/` and keep APIs cohesive.
|
||||
- Do not introduce new runtime dependencies without discussion; prefer standard library and existing crates.
|
||||
- Any change to `src/ffi.rs` must be mirrored in `ffi/medicallib.h` and validated against `examples/c/ffi_example.c`.
|
||||
- Avoid introducing new runtime dependencies; discuss first if unavoidable.
|
||||
|
||||
@@ -35,3 +35,7 @@ opt-level = 3
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = '0.26'
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
if env::var_os("CARGO_FEATURE_FFI").is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed=src/ffi.rs");
|
||||
println!("cargo:rerun-if-changed=cbindgen.toml");
|
||||
|
||||
let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
|
||||
let crate_dir_string = crate_dir
|
||||
.to_str()
|
||||
.expect("crate directory must be valid UTF-8")
|
||||
.to_owned();
|
||||
|
||||
let header_dir = crate_dir.join("ffi");
|
||||
fs::create_dir_all(&header_dir).expect("failed to create ffi output directory");
|
||||
|
||||
let config_path = crate_dir.join("cbindgen.toml");
|
||||
let config = if config_path.exists() {
|
||||
cbindgen::Config::from_file(&config_path)
|
||||
.unwrap_or_else(|err| panic!("failed to load cbindgen config: {err}"))
|
||||
} else {
|
||||
cbindgen::Config::default()
|
||||
};
|
||||
|
||||
let bindings = cbindgen::Builder::new()
|
||||
.with_config(config)
|
||||
.with_crate(crate_dir_string)
|
||||
.generate()
|
||||
.expect("unable to generate cbindgen bindings");
|
||||
|
||||
let mut generated = Vec::new();
|
||||
bindings.write(&mut generated);
|
||||
|
||||
let header_contents = String::from_utf8(generated).expect("generated header was not valid UTF-8");
|
||||
|
||||
let header_path = header_dir.join("medicallib.h");
|
||||
let needs_write = fs::read_to_string(&header_path)
|
||||
.map(|existing| existing != header_contents)
|
||||
.unwrap_or(true);
|
||||
|
||||
if needs_write {
|
||||
fs::write(&header_path, header_contents).expect("failed to write medicallib.h");
|
||||
println!("cargo:warning=Updated ffi/medicallib.h via cbindgen");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
language = "C"
|
||||
include_guard = "MEDICALLIB_RUST_MEDICALLIB_H"
|
||||
pragma_once = true
|
||||
cpp_compat = true
|
||||
usize_is_size_t = true
|
||||
line_length = 100
|
||||
tab_width = 4
|
||||
autogen_warning = "/* This file is generated by cbindgen. Do not edit manually. */"
|
||||
documentation = true
|
||||
|
||||
[parse]
|
||||
expand = ["ffi"]
|
||||
parse_deps = false
|
||||
clean = true
|
||||
+120
-29
@@ -1,55 +1,146 @@
|
||||
// Minimal C header for medicallib_rust FFI
|
||||
// Build with: cargo build --features ffi --release
|
||||
// Link your C app against the produced cdylib and ensure the header codes
|
||||
// match the Rust definitions.
|
||||
#ifndef MEDICALLIB_RUST_MEDICALLIB_H
|
||||
#define MEDICALLIB_RUST_MEDICALLIB_H
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/* This file is generated by cbindgen. Do not edit manually. */
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// Return codes
|
||||
/**
|
||||
* Successful return code.
|
||||
*/
|
||||
#define ML_OK 0
|
||||
|
||||
/**
|
||||
* Generic error.
|
||||
*/
|
||||
#define ML_ERR 1
|
||||
|
||||
/**
|
||||
* Invalid argument.
|
||||
*/
|
||||
#define ML_EINVAL 2
|
||||
|
||||
typedef struct MLPatient MLPatient; // opaque
|
||||
|
||||
// Organ codes (keep in sync with Rust)
|
||||
/**
|
||||
* Organ code for `OrganType::Heart`.
|
||||
*/
|
||||
#define ML_ORGAN_HEART 0
|
||||
|
||||
/**
|
||||
* Organ code for `OrganType::Lungs`.
|
||||
*/
|
||||
#define ML_ORGAN_LUNGS 1
|
||||
|
||||
/**
|
||||
* Organ code for `OrganType::Brain`.
|
||||
*/
|
||||
#define ML_ORGAN_BRAIN 2
|
||||
|
||||
/**
|
||||
* Organ code for `OrganType::SpinalCord`.
|
||||
*/
|
||||
#define ML_ORGAN_SPINAL_CORD 3
|
||||
|
||||
/**
|
||||
* Organ code for `OrganType::Stomach`.
|
||||
*/
|
||||
#define ML_ORGAN_STOMACH 4
|
||||
|
||||
/**
|
||||
* Organ code for `OrganType::Liver`.
|
||||
*/
|
||||
#define ML_ORGAN_LIVER 5
|
||||
|
||||
/**
|
||||
* Organ code for `OrganType::Gallbladder`.
|
||||
*/
|
||||
#define ML_ORGAN_GALLBLADDER 6
|
||||
|
||||
/**
|
||||
* Organ code for `OrganType::Pancreas`.
|
||||
*/
|
||||
#define ML_ORGAN_PANCREAS 7
|
||||
|
||||
/**
|
||||
* Organ code for `OrganType::Intestines`.
|
||||
*/
|
||||
#define ML_ORGAN_INTESTINES 8
|
||||
|
||||
/**
|
||||
* Organ code for `OrganType::Esophagus`.
|
||||
*/
|
||||
#define ML_ORGAN_ESOPHAGUS 9
|
||||
|
||||
/**
|
||||
* Organ code for `OrganType::Kidneys`.
|
||||
*/
|
||||
#define ML_ORGAN_KIDNEYS 10
|
||||
|
||||
/**
|
||||
* Organ code for `OrganType::Bladder`.
|
||||
*/
|
||||
#define ML_ORGAN_BLADDER 11
|
||||
|
||||
/**
|
||||
* Organ code for `OrganType::Spleen`.
|
||||
*/
|
||||
#define ML_ORGAN_SPLEEN 12
|
||||
|
||||
// BMI computation. Returns 0 on success, writes to out_bmi.
|
||||
int32_t medicallib_bmi(float weight_kg, float height_m, float* out_bmi);
|
||||
|
||||
// Patient lifecycle
|
||||
MLPatient* ml_patient_new(const char* id);
|
||||
void ml_patient_free(MLPatient* p);
|
||||
|
||||
// Returns a newly-allocated C string. Free with ml_string_free.
|
||||
char* ml_patient_summary(const MLPatient* p);
|
||||
void ml_string_free(char* s);
|
||||
|
||||
// Advance simulation by dt seconds.
|
||||
int32_t ml_patient_update(MLPatient* p, float dt_seconds);
|
||||
|
||||
// Get summary for a specific organ type code.
|
||||
char* ml_patient_organ_summary(const MLPatient* p, uint32_t organ_code);
|
||||
/**
|
||||
* Opaque patient handle type for C consumers. Wraps a heap-allocated `Patient`.
|
||||
*/
|
||||
typedef struct MLPatient {
|
||||
void *inner;
|
||||
} MLPatient;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
extern "C" {
|
||||
#endif // __cplusplus
|
||||
|
||||
/**
|
||||
* Compute BMI; returns 0 on success, non-zero on error.
|
||||
* On success writes result to `out_bmi`.
|
||||
*/
|
||||
int32_t medicallib_bmi(float weight_kg, float height_m, float *out_bmi);
|
||||
|
||||
/**
|
||||
* Create a new patient with id string. Returns null on error.
|
||||
*/
|
||||
struct MLPatient *ml_patient_new(const char *id);
|
||||
|
||||
/**
|
||||
* Destroy a patient handle. Accepts null.
|
||||
*/
|
||||
void ml_patient_free(struct MLPatient *p);
|
||||
|
||||
/**
|
||||
* Get a newly-allocated C string summary for the patient.
|
||||
* Returns null on error. Free the returned string with `ml_string_free`.
|
||||
*/
|
||||
char *ml_patient_summary(const struct MLPatient *p);
|
||||
|
||||
/**
|
||||
* Frees a C string previously returned by this library. Accepts null.
|
||||
*/
|
||||
void ml_string_free(char *s);
|
||||
|
||||
/**
|
||||
* Advance patient simulation by dt seconds.
|
||||
*/
|
||||
int32_t ml_patient_update(struct MLPatient *p, float dt_seconds);
|
||||
|
||||
/**
|
||||
* Return organ summary by type code. See header for codes. Caller frees string.
|
||||
*/
|
||||
char *ml_patient_organ_summary(const struct MLPatient *p, uint32_t organ_code);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif // __cplusplus
|
||||
|
||||
#endif /* MEDICALLIB_RUST_MEDICALLIB_H */
|
||||
|
||||
+74
-28
@@ -7,7 +7,7 @@
|
||||
#![allow(unsafe_code)]
|
||||
#![allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
|
||||
use core::ffi::{c_char, CStr};
|
||||
use core::ffi::{c_char, c_void, CStr};
|
||||
use std::ffi::CString;
|
||||
use std::ptr;
|
||||
|
||||
@@ -21,11 +21,38 @@ pub const ML_ERR: i32 = 1;
|
||||
/// Invalid argument.
|
||||
pub const ML_EINVAL: i32 = 2;
|
||||
|
||||
/// Opaque patient handle type for C consumers.
|
||||
/// Organ code for `OrganType::Heart`.
|
||||
pub const ML_ORGAN_HEART: u32 = 0;
|
||||
/// Organ code for `OrganType::Lungs`.
|
||||
pub const ML_ORGAN_LUNGS: u32 = 1;
|
||||
/// Organ code for `OrganType::Brain`.
|
||||
pub const ML_ORGAN_BRAIN: u32 = 2;
|
||||
/// Organ code for `OrganType::SpinalCord`.
|
||||
pub const ML_ORGAN_SPINAL_CORD: u32 = 3;
|
||||
/// Organ code for `OrganType::Stomach`.
|
||||
pub const ML_ORGAN_STOMACH: u32 = 4;
|
||||
/// Organ code for `OrganType::Liver`.
|
||||
pub const ML_ORGAN_LIVER: u32 = 5;
|
||||
/// Organ code for `OrganType::Gallbladder`.
|
||||
pub const ML_ORGAN_GALLBLADDER: u32 = 6;
|
||||
/// Organ code for `OrganType::Pancreas`.
|
||||
pub const ML_ORGAN_PANCREAS: u32 = 7;
|
||||
/// Organ code for `OrganType::Intestines`.
|
||||
pub const ML_ORGAN_INTESTINES: u32 = 8;
|
||||
/// Organ code for `OrganType::Esophagus`.
|
||||
pub const ML_ORGAN_ESOPHAGUS: u32 = 9;
|
||||
/// Organ code for `OrganType::Kidneys`.
|
||||
pub const ML_ORGAN_KIDNEYS: u32 = 10;
|
||||
/// Organ code for `OrganType::Bladder`.
|
||||
pub const ML_ORGAN_BLADDER: u32 = 11;
|
||||
/// Organ code for `OrganType::Spleen`.
|
||||
pub const ML_ORGAN_SPLEEN: u32 = 12;
|
||||
|
||||
/// Opaque patient handle type for C consumers. Wraps a heap-allocated `Patient`.
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
pub struct MLPatient {
|
||||
inner: Patient,
|
||||
inner: *mut c_void,
|
||||
}
|
||||
|
||||
/// Compute BMI; returns 0 on success, non-zero on error.
|
||||
@@ -51,15 +78,17 @@ pub extern "C" fn ml_patient_new(id: *const c_char) -> *mut MLPatient {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let cstr = unsafe { CStr::from_ptr(id) };
|
||||
match cstr.to_str() {
|
||||
let patient = match cstr.to_str() {
|
||||
Ok(s) => match Patient::new(s) {
|
||||
Ok(p) => Box::into_raw(Box::new(MLPatient {
|
||||
inner: p.initialize_default(),
|
||||
})),
|
||||
Err(_) => ptr::null_mut(),
|
||||
Ok(p) => p.initialize_default(),
|
||||
Err(_) => return ptr::null_mut(),
|
||||
},
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
Err(_) => return ptr::null_mut(),
|
||||
};
|
||||
let patient_ptr: *mut Patient = Box::into_raw(Box::new(patient));
|
||||
Box::into_raw(Box::new(MLPatient {
|
||||
inner: patient_ptr.cast::<c_void>(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Destroy a patient handle. Accepts null.
|
||||
@@ -69,7 +98,12 @@ pub extern "C" fn ml_patient_free(p: *mut MLPatient) {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
drop(Box::from_raw(p));
|
||||
let mut handle = Box::from_raw(p);
|
||||
if !handle.inner.is_null() {
|
||||
let patient_ptr = handle.inner as *mut Patient;
|
||||
drop(Box::from_raw(patient_ptr));
|
||||
handle.inner = ptr::null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +114,11 @@ pub extern "C" fn ml_patient_summary(p: *const MLPatient) -> *mut c_char {
|
||||
if p.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let summary = unsafe { (*p).inner.patient_summary() };
|
||||
let patient_ptr = unsafe { (*p).inner as *const Patient };
|
||||
if patient_ptr.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let summary = unsafe { (*patient_ptr).patient_summary() };
|
||||
match CString::new(summary) {
|
||||
Ok(s) => s.into_raw(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
@@ -104,8 +142,12 @@ pub extern "C" fn ml_patient_update(p: *mut MLPatient, dt_seconds: f32) -> i32 {
|
||||
if p.is_null() {
|
||||
return ML_EINVAL;
|
||||
}
|
||||
let patient_ptr = unsafe { (*p).inner as *mut Patient };
|
||||
if patient_ptr.is_null() {
|
||||
return ML_EINVAL;
|
||||
}
|
||||
unsafe {
|
||||
(*p).inner.update(dt_seconds);
|
||||
(*patient_ptr).update(dt_seconds);
|
||||
}
|
||||
ML_OK
|
||||
}
|
||||
@@ -116,25 +158,29 @@ pub extern "C" fn ml_patient_organ_summary(p: *const MLPatient, organ_code: u32)
|
||||
if p.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let patient_ptr = unsafe { (*p).inner as *const Patient };
|
||||
if patient_ptr.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let kind = match organ_code {
|
||||
0 => OrganType::Heart,
|
||||
1 => OrganType::Lungs,
|
||||
2 => OrganType::Brain,
|
||||
3 => OrganType::SpinalCord,
|
||||
4 => OrganType::Stomach,
|
||||
5 => OrganType::Liver,
|
||||
6 => OrganType::Gallbladder,
|
||||
7 => OrganType::Pancreas,
|
||||
8 => OrganType::Intestines,
|
||||
9 => OrganType::Esophagus,
|
||||
10 => OrganType::Kidneys,
|
||||
11 => OrganType::Bladder,
|
||||
12 => OrganType::Spleen,
|
||||
ML_ORGAN_HEART => OrganType::Heart,
|
||||
ML_ORGAN_LUNGS => OrganType::Lungs,
|
||||
ML_ORGAN_BRAIN => OrganType::Brain,
|
||||
ML_ORGAN_SPINAL_CORD => OrganType::SpinalCord,
|
||||
ML_ORGAN_STOMACH => OrganType::Stomach,
|
||||
ML_ORGAN_LIVER => OrganType::Liver,
|
||||
ML_ORGAN_GALLBLADDER => OrganType::Gallbladder,
|
||||
ML_ORGAN_PANCREAS => OrganType::Pancreas,
|
||||
ML_ORGAN_INTESTINES => OrganType::Intestines,
|
||||
ML_ORGAN_ESOPHAGUS => OrganType::Esophagus,
|
||||
ML_ORGAN_KIDNEYS => OrganType::Kidneys,
|
||||
ML_ORGAN_BLADDER => OrganType::Bladder,
|
||||
ML_ORGAN_SPLEEN => OrganType::Spleen,
|
||||
_ => return ptr::null_mut(),
|
||||
};
|
||||
let summary = unsafe { (*p).inner.organ_summary(kind) };
|
||||
let summary = unsafe { (*patient_ptr).organ_summary(kind) };
|
||||
match summary {
|
||||
Ok(s) => match CString::new(s) {
|
||||
Ok(text) => match CString::new(text) {
|
||||
Ok(cs) => cs.into_raw(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user