mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
solidcraft-smlib: SMLib statically linked behind its C API, with a safe wrapper
A new crate (L1, only the kernel uses it) builds tangency-labs/smlib's SMLib, SM_API and C API with the cc crate when its `native` feature is on, from the commit in crates/smlib/SMLIB_REV (scripts/fetch-smlib.sh fetches just those sources). The kernel's `smlib` feature turns it on. Without it nothing C++ is built and every call returns NotBuilt, so workspace builds need no C++ toolchain. unsafe is confined to src/ffi.rs. The wrapper owns handles, maps statuses to errors, checks the C API version, and sends SMLib's per-call log to the log crate. Crash reports note the native kernel call in flight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Generated
+9
@@ -4366,6 +4366,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-smlib",
|
||||
"spade",
|
||||
"thiserror 2.0.21",
|
||||
"truck-meshalgo",
|
||||
@@ -4410,6 +4411,14 @@ dependencies = [
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-smlib"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-space-mouse"
|
||||
version = "0.24.0"
|
||||
|
||||
@@ -18,6 +18,7 @@ robust = "1.2"
|
||||
solidcraft-geom = { path = "crates/geom" }
|
||||
solidcraft-sketch = { path = "crates/sketch" }
|
||||
solidcraft-kernel = { path = "crates/kernel" }
|
||||
solidcraft-smlib = { path = "crates/smlib" }
|
||||
solidcraft-render = { path = "crates/render" }
|
||||
solidcraft-gpu = { path = "crates/gpu" }
|
||||
solidcraft-doc = { path = "crates/doc" }
|
||||
|
||||
@@ -1011,6 +1011,9 @@ fn write_report(state: &State, kind: &str, p: Option<&Panic>, extra: Extra) -> O
|
||||
fs::create_dir_all(&folder).ok()?;
|
||||
let mut files = serde_json::Map::new();
|
||||
let mut notes = extra.notes;
|
||||
if let Some(call) = solidcraft_kernel::native_call_in_flight() {
|
||||
notes.push(format!("native kernel call in flight: {call}"));
|
||||
}
|
||||
if let Some(d) = &extra.design {
|
||||
if d.len() <= MAX_DESIGN_BYTES {
|
||||
if solidcraft_io::write_atomic(&folder.join("design.solidcraft"), d, false).is_ok() {
|
||||
|
||||
@@ -8,8 +8,14 @@ license.workspace = true
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
# Link NVIDIA's SMLib (tangency-labs/smlib) into the kernel; needs a C++ toolchain and
|
||||
# scripts/fetch-smlib.sh (ADR 0002).
|
||||
smlib = ["dep:solidcraft-smlib", "solidcraft-smlib/native"]
|
||||
|
||||
[dependencies]
|
||||
solidcraft-geom = { workspace = true }
|
||||
solidcraft-smlib = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -240,6 +240,18 @@ pub enum KernelError {
|
||||
|
||||
pub type Result<T> = std::result::Result<T, KernelError>;
|
||||
|
||||
/// The native kernel call running right now (SMLib, with the `smlib` feature), for crash reports.
|
||||
pub fn native_call_in_flight() -> Option<String> {
|
||||
#[cfg(feature = "smlib")]
|
||||
{
|
||||
solidcraft_smlib::current_call()
|
||||
}
|
||||
#[cfg(not(feature = "smlib"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static GUARDED: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "solidcraft-smlib"
|
||||
description = "SolidCraft: NVIDIA's SMLib B-rep kernel (tangency-labs/smlib, Apache-2.0), statically linked behind its C API, with a safe Rust wrapper"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
links = "smlib"
|
||||
|
||||
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/ffi.rs can allow it.
|
||||
# Calling a C library has no safe API. Keep the rest in sync with [workspace.lints].
|
||||
[lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
[lints.clippy]
|
||||
dbg_macro = "warn"
|
||||
todo = "warn"
|
||||
too_many_arguments = "allow"
|
||||
type_complexity = "allow"
|
||||
neg_cmp_op_on_partial_ord = "allow"
|
||||
|
||||
[features]
|
||||
# Compile and link SMLib. Off, the crate builds without a C++ toolchain or the SMLib sources, and
|
||||
# every call returns `Error::NotBuilt`, so workspace-wide builds and checks need neither.
|
||||
native = []
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
cc = { version = "1.2", features = ["parallel"] }
|
||||
@@ -0,0 +1 @@
|
||||
c21f4b0d7ad0da28c9d2ce2bc0b23ef3929704ef
|
||||
@@ -0,0 +1,92 @@
|
||||
//! Builds SMLib, SM_API and their C API (tangency-labs/smlib, at the commit in `SMLIB_REV`) into
|
||||
//! static libraries, when the `native` feature is on. The sources come from
|
||||
//! `SOLIDCRAFT_SMLIB_DIR`, or from `target/smlib-<rev>` as `scripts/fetch-smlib.sh` leaves them.
|
||||
//!
|
||||
//! The file lists, defines and include order follow the fork's `capi/CMakeLists.txt`: SMLib/inc
|
||||
//! and SM_API/inc each have a StdAfx.h, so each part must find its own first.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=SMLIB_REV");
|
||||
println!("cargo:rerun-if-env-changed=SOLIDCRAFT_SMLIB_DIR");
|
||||
if std::env::var_os("CARGO_FEATURE_NATIVE").is_none() {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = build() {
|
||||
// A build script has no other way to fail the build.
|
||||
eprintln!("error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn build() -> Result<(), String> {
|
||||
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").map_err(|e| e.to_string())?);
|
||||
let rev = std::fs::read_to_string(manifest.join("SMLIB_REV")).map_err(|e| format!("SMLIB_REV: {e}"))?.trim().to_string();
|
||||
let root = match std::env::var_os("SOLIDCRAFT_SMLIB_DIR") {
|
||||
Some(d) => PathBuf::from(d),
|
||||
None => manifest.join("../../target").join(format!("smlib-{}", rev.get(..12).unwrap_or(&rev))),
|
||||
};
|
||||
if !root.join("capi/include/smlib.h").is_file() {
|
||||
return Err(format!(
|
||||
"the SMLib sources are not at {} (feature `native`): run scripts/fetch-smlib.sh, or set SOLIDCRAFT_SMLIB_DIR to a tangency-labs/smlib checkout at {rev}",
|
||||
root.display()
|
||||
));
|
||||
}
|
||||
println!("cargo:rerun-if-changed={}", root.join("capi").display());
|
||||
let src = root.join("source");
|
||||
let core = sources(&src.join("SMLib/src"), &["StdAfx.cpp", "SmDllMain.cpp"])?;
|
||||
let api = sources(&src.join("SM_API/src"), &["StdAfx.cpp"])?;
|
||||
let target = std::env::var("TARGET").unwrap_or_default();
|
||||
let msvc = target.contains("msvc");
|
||||
|
||||
let base = || {
|
||||
let mut b = cc::Build::new();
|
||||
b.cpp(true).std("c++17").define("NDEBUG", None).define("NOMINMAX", None).define("SM_STATIC", None).define("SM_RAND_SEED", "20240101u");
|
||||
// Rust links the MSVC runtime as a DLL; the cc crate's default (/MD) matches.
|
||||
if msvc {
|
||||
b.flag("/EHsc").flag("/bigobj");
|
||||
}
|
||||
b
|
||||
};
|
||||
let quiet = |b: &mut cc::Build| {
|
||||
b.warnings(false);
|
||||
if msvc {
|
||||
b.flag("/w");
|
||||
} else {
|
||||
b.flag("-w");
|
||||
}
|
||||
};
|
||||
|
||||
// Link order: the C API needs SM_API, which needs SMLib.
|
||||
let mut c = base();
|
||||
c.file(root.join("capi/src/smlib_c.cpp")).include(root.join("capi/include")).include(src.join("SM_API/inc")).include(src.join("SMLib/inc"));
|
||||
quiet(&mut c);
|
||||
c.try_compile("smlib_capi").map_err(|e| e.to_string())?;
|
||||
|
||||
let mut a = base();
|
||||
a.files(&api).include(src.join("SM_API/inc")).include(src.join("SMLib/inc"));
|
||||
quiet(&mut a);
|
||||
a.try_compile("smlib_api").map_err(|e| e.to_string())?;
|
||||
|
||||
let mut k = base();
|
||||
k.files(&core).include(src.join("SMLib/inc"));
|
||||
quiet(&mut k);
|
||||
k.try_compile("smlib_core").map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The `.cpp` files in `dir`, sorted, without `skip`.
|
||||
fn sources(dir: &Path, skip: &[&str]) -> Result<Vec<PathBuf>, String> {
|
||||
let mut v: Vec<PathBuf> = std::fs::read_dir(dir)
|
||||
.map_err(|e| format!("{}: {e}", dir.display()))?
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "cpp") && !p.file_name().is_some_and(|n| skip.iter().any(|s| n == *s)))
|
||||
.collect();
|
||||
v.sort();
|
||||
if v.is_empty() {
|
||||
return Err(format!("no sources in {}", dir.display()));
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
//! The C API of `capi/include/smlib.h` (tangency-labs/smlib), declared by hand. The only place in
|
||||
//! the crate with `unsafe`: every function here is called through the safe wrappers in the crate
|
||||
//! root, which check their arguments and own the handles.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::ffi::{c_char, c_void};
|
||||
|
||||
/// `SMLIB_ABI_VERSION` this crate was written against.
|
||||
pub const ABI_VERSION: i32 = 1;
|
||||
|
||||
pub const OK: i32 = 0;
|
||||
pub const ERR_EXCEPTION: i32 = -1000;
|
||||
pub const ERR_BUFFER: i32 = -1001;
|
||||
pub const ERR_ARG: i32 = -1002;
|
||||
pub const ERR_EMPTY: i32 = -1003;
|
||||
|
||||
/// `smlib_brep`, opaque.
|
||||
#[repr(C)]
|
||||
pub struct RawBrep {
|
||||
_private: [u8; 0],
|
||||
}
|
||||
|
||||
pub type LogFn = extern "C" fn(user: *mut c_void, level: i32, line: *const c_char);
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
unsafe extern "C" {
|
||||
pub fn smlib_abi_version() -> i32;
|
||||
pub fn smlib_init() -> i32;
|
||||
pub fn smlib_set_log(log: Option<LogFn>, user: *mut c_void);
|
||||
pub fn smlib_set_dump_dir(dir: *const c_char) -> i32;
|
||||
pub fn smlib_current_call(buf: *mut c_char, cap: i64) -> i64;
|
||||
pub fn smlib_brep_read(path: *const c_char, ascii: i32, out: *mut *mut RawBrep) -> i32;
|
||||
pub fn smlib_brep_write(brep: *const RawBrep, path: *const c_char, ascii: i32) -> i32;
|
||||
pub fn smlib_brep_free(brep: *mut RawBrep);
|
||||
pub fn smlib_brep_copy(brep: *const RawBrep, out: *mut *mut RawBrep) -> i32;
|
||||
pub fn smlib_make_box(origin: *const f64, size: *const f64, out: *mut *mut RawBrep) -> i32;
|
||||
pub fn smlib_brep_volume(brep: *const RawBrep, relative_accuracy: f64, out: *mut f64) -> i32;
|
||||
pub fn smlib_brep_counts(brep: *const RawBrep, out: *mut i64) -> i32;
|
||||
}
|
||||
|
||||
/// Returned by every call when the crate was built without the `native` feature.
|
||||
pub const NOT_BUILT: i32 = -2000;
|
||||
|
||||
/// An owned `smlib_brep`. Only made here, from a pointer the C API returned; freed once on drop.
|
||||
#[cfg_attr(not(feature = "native"), allow(dead_code))]
|
||||
pub struct Handle(std::ptr::NonNull<RawBrep>);
|
||||
|
||||
// SMLib serialises every call behind its own lock, and a handle is only ever used through it.
|
||||
unsafe impl Send for Handle {}
|
||||
unsafe impl Sync for Handle {}
|
||||
|
||||
impl Drop for Handle {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(feature = "native")]
|
||||
unsafe {
|
||||
smlib_brep_free(self.0.as_ptr())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
#[cfg_attr(not(feature = "native"), allow(dead_code))]
|
||||
fn raw(&self) -> *const RawBrep {
|
||||
self.0.as_ptr()
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs a call that returns a new B-rep through `out`.
|
||||
#[cfg(feature = "native")]
|
||||
fn new_brep(f: impl FnOnce(*mut *mut RawBrep) -> i32) -> Result<Handle, i32> {
|
||||
let mut out: *mut RawBrep = std::ptr::null_mut();
|
||||
let s = f(&mut out);
|
||||
match (s, std::ptr::NonNull::new(out)) {
|
||||
(OK, Some(p)) => Ok(Handle(p)),
|
||||
(OK, None) => Err(ERR_EMPTY),
|
||||
(s, _) => Err(s),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
mod calls {
|
||||
use super::*;
|
||||
use std::ffi::CStr;
|
||||
|
||||
pub fn abi_version() -> i32 {
|
||||
unsafe { smlib_abi_version() }
|
||||
}
|
||||
pub fn init() -> i32 {
|
||||
unsafe { smlib_init() }
|
||||
}
|
||||
pub fn set_log(log: Option<LogFn>) {
|
||||
unsafe { smlib_set_log(log, std::ptr::null_mut()) }
|
||||
}
|
||||
pub fn set_dump_dir(dir: Option<&CStr>) -> i32 {
|
||||
unsafe { smlib_set_dump_dir(dir.map_or(std::ptr::null(), CStr::as_ptr)) }
|
||||
}
|
||||
pub fn current_call(buf: &mut [u8]) -> usize {
|
||||
let cap = i64::try_from(buf.len()).unwrap_or(i64::MAX);
|
||||
let n = unsafe { smlib_current_call(buf.as_mut_ptr().cast(), cap) };
|
||||
usize::try_from(n).unwrap_or(0).min(buf.len().saturating_sub(1))
|
||||
}
|
||||
pub fn read(path: &CStr, ascii: bool) -> Result<Handle, i32> {
|
||||
new_brep(|out| unsafe { smlib_brep_read(path.as_ptr(), i32::from(ascii), out) })
|
||||
}
|
||||
pub fn write(b: &Handle, path: &CStr, ascii: bool) -> i32 {
|
||||
unsafe { smlib_brep_write(b.raw(), path.as_ptr(), i32::from(ascii)) }
|
||||
}
|
||||
pub fn copy(b: &Handle) -> Result<Handle, i32> {
|
||||
new_brep(|out| unsafe { smlib_brep_copy(b.raw(), out) })
|
||||
}
|
||||
pub fn make_box(origin: &[f64; 3], size: &[f64; 3]) -> Result<Handle, i32> {
|
||||
new_brep(|out| unsafe { smlib_make_box(origin.as_ptr(), size.as_ptr(), out) })
|
||||
}
|
||||
pub fn volume(b: &Handle, relative_accuracy: f64) -> Result<f64, i32> {
|
||||
let mut v = 0.0;
|
||||
match unsafe { smlib_brep_volume(b.raw(), relative_accuracy, &mut v) } {
|
||||
OK => Ok(v),
|
||||
s => Err(s),
|
||||
}
|
||||
}
|
||||
pub fn counts(b: &Handle) -> Result<[i64; 3], i32> {
|
||||
let mut c = [0i64; 3];
|
||||
match unsafe { smlib_brep_counts(b.raw(), c.as_mut_ptr()) } {
|
||||
OK => Ok(c),
|
||||
s => Err(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The same calls when SMLib is not built: each one fails with [`NOT_BUILT`].
|
||||
#[cfg(not(feature = "native"))]
|
||||
mod calls {
|
||||
use super::*;
|
||||
use std::ffi::CStr;
|
||||
|
||||
pub fn abi_version() -> i32 {
|
||||
NOT_BUILT
|
||||
}
|
||||
pub fn init() -> i32 {
|
||||
NOT_BUILT
|
||||
}
|
||||
pub fn set_log(_: Option<LogFn>) {}
|
||||
pub fn set_dump_dir(_: Option<&CStr>) -> i32 {
|
||||
NOT_BUILT
|
||||
}
|
||||
pub fn current_call(_: &mut [u8]) -> usize {
|
||||
0
|
||||
}
|
||||
pub fn read(_: &CStr, _: bool) -> Result<Handle, i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
pub fn write(_: &Handle, _: &CStr, _: bool) -> i32 {
|
||||
NOT_BUILT
|
||||
}
|
||||
pub fn copy(_: &Handle) -> Result<Handle, i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
pub fn make_box(_: &[f64; 3], _: &[f64; 3]) -> Result<Handle, i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
pub fn volume(_: &Handle, _: f64) -> Result<f64, i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
pub fn counts(_: &Handle) -> Result<[i64; 3], i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
}
|
||||
|
||||
pub use calls::*;
|
||||
|
||||
/// A log line from the C API: NUL-terminated and valid for the callback's duration.
|
||||
pub fn line(p: *const c_char) -> String {
|
||||
if p.is_null() {
|
||||
return String::new();
|
||||
}
|
||||
unsafe { std::ffi::CStr::from_ptr(p) }.to_string_lossy().into_owned()
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
//! NVIDIA's SMLib B-rep kernel (tangency-labs/smlib, Apache-2.0), statically linked behind its C
|
||||
//! API, with a safe Rust wrapper. Only `crates/kernel` uses this crate (ADR 0002).
|
||||
//!
|
||||
//! The C API never lets a C++ exception, type or pointer out, never changes its inputs, and
|
||||
//! serialises every call behind one lock (SMLib keeps process-global state). This wrapper owns
|
||||
//! the handles, turns status codes into [`Error`], and sends SMLib's one-line-per-call log to the
|
||||
//! `log` crate (target `smlib`).
|
||||
//!
|
||||
//! Built without the `native` feature, the crate needs no C++ toolchain and every call returns
|
||||
//! [`Error::NotBuilt`].
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
use std::ffi::{CString, c_char, c_void};
|
||||
use std::path::Path;
|
||||
use std::sync::Once;
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
mod ffi;
|
||||
|
||||
/// Why an SMLib call failed.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// SMLib's own status (1001 `SM_ERR`, 1007 `SM_ERR_INVALID_INPUT`, …).
|
||||
Status(i32),
|
||||
/// An argument the C API rejected before SMLib saw it.
|
||||
Argument,
|
||||
/// SMLib reported success but returned no body.
|
||||
Empty,
|
||||
/// A C++ exception (most likely out of memory) was caught at the boundary.
|
||||
Exception,
|
||||
/// The C API is a different version from the one this crate was written against.
|
||||
Version(i32),
|
||||
/// This build has no SMLib (the `native` feature is off).
|
||||
NotBuilt,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::Status(1007) => write!(f, "SMLib rejected the input (SM_ERR_INVALID_INPUT)"),
|
||||
Error::Status(s) => write!(f, "SMLib failed (status {s})"),
|
||||
Error::Argument => write!(f, "invalid argument for SMLib"),
|
||||
Error::Empty => write!(f, "SMLib returned an empty body"),
|
||||
Error::Exception => write!(f, "SMLib raised an internal exception"),
|
||||
Error::Version(v) => write!(f, "SMLib C API version {v}, expected {}", ffi::ABI_VERSION),
|
||||
Error::NotBuilt => write!(f, "not supported yet: this build has no SMLib"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
fn error(s: i32) -> Error {
|
||||
match s {
|
||||
ffi::ERR_ARG | ffi::ERR_BUFFER => Error::Argument,
|
||||
ffi::ERR_EMPTY => Error::Empty,
|
||||
ffi::ERR_EXCEPTION => Error::Exception,
|
||||
ffi::NOT_BUILT => Error::NotBuilt,
|
||||
s => Error::Status(s),
|
||||
}
|
||||
}
|
||||
|
||||
fn check(s: i32) -> Result<()> {
|
||||
if s == ffi::OK { Ok(()) } else { Err(error(s)) }
|
||||
}
|
||||
|
||||
fn c_path(p: &Path) -> Result<CString> {
|
||||
CString::new(p.to_string_lossy().as_bytes()).map_err(|_| Error::Argument)
|
||||
}
|
||||
|
||||
extern "C" fn log_line(_: *mut c_void, level: i32, line: *const c_char) {
|
||||
let text = ffi::line(line);
|
||||
if level == 0 {
|
||||
log::debug!(target: "smlib", "{text}");
|
||||
} else {
|
||||
log::warn!(target: "smlib", "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
/// What [`init`] found, kept for every later call (0: ready).
|
||||
static INIT_RESULT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
/// Checks the C API's version, creates SMLib's context and routes its log to the `log` crate.
|
||||
/// Every other call runs it first; calling it at start-up keeps the cost out of the first operation.
|
||||
pub fn init() -> Result<()> {
|
||||
INIT.call_once(|| {
|
||||
let v = ffi::abi_version();
|
||||
let result = if v == ffi::NOT_BUILT {
|
||||
v
|
||||
} else if v != ffi::ABI_VERSION {
|
||||
v.saturating_add(10_000)
|
||||
} else {
|
||||
ffi::set_log(Some(log_line));
|
||||
ffi::init()
|
||||
};
|
||||
INIT_RESULT.store(result, Ordering::Release);
|
||||
});
|
||||
match INIT_RESULT.load(Ordering::Acquire) {
|
||||
ffi::OK => Ok(()),
|
||||
v if v >= 10_000 => Err(Error::Version(v - 10_000)),
|
||||
s => Err(error(s)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where failing calls leave their inputs for replay (see the C API's `smlib_set_dump_dir`);
|
||||
/// `None` turns that off.
|
||||
pub fn set_dump_dir(dir: Option<&Path>) -> Result<()> {
|
||||
init()?;
|
||||
let c = dir.map(c_path).transpose()?;
|
||||
check(ffi::set_dump_dir(c.as_deref()))
|
||||
}
|
||||
|
||||
/// The SMLib call running right now, if any: for crash reports. Takes no lock.
|
||||
pub fn current_call() -> Option<String> {
|
||||
let mut buf = [0u8; 512];
|
||||
let n = ffi::current_call(&mut buf);
|
||||
let s = buf.get(..n).map(|b| String::from_utf8_lossy(b).into_owned()).unwrap_or_default();
|
||||
(!s.is_empty()).then_some(s)
|
||||
}
|
||||
|
||||
/// An SMLib B-rep. Immutable: operations return new bodies.
|
||||
pub struct Brep(ffi::Handle);
|
||||
|
||||
impl std::fmt::Debug for Brep {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("smlib::Brep")
|
||||
}
|
||||
}
|
||||
|
||||
impl Brep {
|
||||
/// An axis-aligned box from its lower corner, with positive edge lengths (mm).
|
||||
pub fn make_box(origin: [f64; 3], size: [f64; 3]) -> Result<Brep> {
|
||||
init()?;
|
||||
ffi::make_box(&origin, &size).map(Brep).map_err(error)
|
||||
}
|
||||
|
||||
/// Reads a native SMLib `.smb` file.
|
||||
pub fn read(path: &Path, ascii: bool) -> Result<Brep> {
|
||||
init()?;
|
||||
ffi::read(&c_path(path)?, ascii).map(Brep).map_err(error)
|
||||
}
|
||||
|
||||
/// Writes this body to a native SMLib `.smb` file.
|
||||
pub fn write(&self, path: &Path, ascii: bool) -> Result<()> {
|
||||
check(ffi::write(&self.0, &c_path(path)?, ascii))
|
||||
}
|
||||
|
||||
/// A deep copy.
|
||||
pub fn try_clone(&self) -> Result<Brep> {
|
||||
ffi::copy(&self.0).map(Brep).map_err(error)
|
||||
}
|
||||
|
||||
/// The enclosed volume (mm³) at `relative_accuracy` (SMLib clamps it to [1e-8, 1e-1]).
|
||||
pub fn volume(&self, relative_accuracy: f64) -> Result<f64> {
|
||||
ffi::volume(&self.0, relative_accuracy).map_err(error)
|
||||
}
|
||||
|
||||
/// Face, edge and vertex counts.
|
||||
pub fn counts(&self) -> Result<[usize; 3]> {
|
||||
let c = ffi::counts(&self.0).map_err(error)?;
|
||||
Ok(c.map(|n| usize::try_from(n).unwrap_or(0)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,70 @@
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
#[test]
|
||||
fn box_end_to_end() {
|
||||
let b = Brep::make_box([1.0, 2.0, 3.0], [10.0, 20.0, 30.0]).unwrap();
|
||||
assert!((b.volume(1e-7).unwrap() - 6000.0).abs() < 1e-6);
|
||||
assert_eq!(b.counts().unwrap(), [6, 12, 8]);
|
||||
let c = b.try_clone().unwrap();
|
||||
drop(b);
|
||||
assert!((c.volume(1e-7).unwrap() - 6000.0).abs() < 1e-6);
|
||||
assert_eq!(current_call(), None, "no call in flight between calls");
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
#[test]
|
||||
fn bad_arguments_are_errors() {
|
||||
assert_eq!(Brep::make_box([0.0; 3], [1.0, -1.0, 1.0]).unwrap_err(), Error::Argument);
|
||||
assert_eq!(Brep::make_box([f64::NAN, 0.0, 0.0], [1.0; 3]).unwrap_err(), Error::Argument);
|
||||
let b = Brep::make_box([0.0; 3], [1.0; 3]).unwrap();
|
||||
assert_eq!(b.volume(f64::INFINITY).unwrap_err(), Error::Argument);
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
#[test]
|
||||
fn native_files_round_trip_and_failures_dump_their_inputs() {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-smlib-test-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let b = Brep::make_box([0.0; 3], [2.0, 3.0, 4.0]).unwrap();
|
||||
let path = dir.join("box.smb");
|
||||
b.write(&path, false).unwrap();
|
||||
let back = Brep::read(&path, false).unwrap();
|
||||
assert!((back.volume(1e-7).unwrap() - 24.0).abs() < 1e-9);
|
||||
|
||||
let dumps = dir.join("dumps");
|
||||
std::fs::create_dir_all(&dumps).unwrap();
|
||||
set_dump_dir(Some(&dumps)).unwrap();
|
||||
assert!(b.write(&dir.join("missing/x.smb"), false).is_err());
|
||||
set_dump_dir(None).unwrap();
|
||||
let names: Vec<String> =
|
||||
std::fs::read_dir(&dumps).unwrap().filter_map(|e| e.ok()).map(|e| e.file_name().to_string_lossy().into_owned()).collect();
|
||||
assert!(names.iter().any(|n| n.ends_with("-smlib_brep_write.txt")), "{names:?}");
|
||||
let smb = names.iter().find(|n| n.ends_with("-smlib_brep_write-brep.smb")).unwrap();
|
||||
let replayed = Brep::read(&dumps.join(smb), false).unwrap();
|
||||
assert!((replayed.volume(1e-7).unwrap() - 24.0).abs() < 1e-9);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
#[test]
|
||||
fn calls_from_many_threads_are_serialised() {
|
||||
let handles: Vec<_> = (0..8)
|
||||
.map(|i| {
|
||||
std::thread::spawn(move || {
|
||||
let s = 1.0 + i as f64;
|
||||
let b = Brep::make_box([0.0; 3], [s; 3]).unwrap();
|
||||
(b.volume(1e-7).unwrap() - s * s * s).abs() < 1e-9 * s * s * s
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
assert!(handles.into_iter().all(|h| h.join().unwrap()));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "native"))]
|
||||
#[test]
|
||||
fn without_native_every_call_says_so() {
|
||||
assert_eq!(init(), Err(Error::NotBuilt));
|
||||
assert_eq!(Brep::make_box([0.0; 3], [1.0; 3]).unwrap_err(), Error::NotBuilt);
|
||||
assert_eq!(current_call(), None);
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fetches the SMLib sources solidcraft-smlib builds (feature `native`): tangency-labs/smlib at
|
||||
# the commit in crates/smlib/SMLIB_REV, only the parts the build reads (source/SMLib,
|
||||
# source/SM_API, capi), into target/smlib-<rev12>. OpenCASCADE is not part of the fork.
|
||||
set -euo pipefail
|
||||
root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
rev="$(tr -d '[:space:]' < "$root/crates/smlib/SMLIB_REV")"
|
||||
dest="${SOLIDCRAFT_SMLIB_DIR:-$root/target/smlib-${rev:0:12}}"
|
||||
if [ -f "$dest/capi/include/smlib.h" ] && [ "$(git -C "$dest" rev-parse HEAD 2>/dev/null)" = "$rev" ]; then
|
||||
echo "SMLib $rev already at $dest"
|
||||
exit 0
|
||||
fi
|
||||
rm -rf "$dest"
|
||||
mkdir -p "$dest"
|
||||
git -C "$dest" init -q
|
||||
git -C "$dest" remote add origin https://github.com/tangency-labs/smlib.git
|
||||
git -C "$dest" sparse-checkout set --no-cone /source/SMLib/ /source/SM_API/ /capi/ /LICENSE
|
||||
git -C "$dest" fetch -q --depth 1 --filter=blob:none origin "$rev"
|
||||
git -C "$dest" checkout -q FETCH_HEAD
|
||||
echo "SMLib $rev at $dest"
|
||||
+3
-1
@@ -43,6 +43,8 @@ pub const TABLE: &[(&str, Class)] = &[
|
||||
// the 3Dconnexion driver's client API on macOS (no workspace dependencies)
|
||||
("3dx-client", Class::Standalone),
|
||||
("sketch", Class::Layer(1)),
|
||||
// NVIDIA's SMLib behind its C API (kernel-smlib branch, ADR 0002); only the kernel uses it
|
||||
("smlib", Class::Layer(1)),
|
||||
("kernel", Class::Layer(1)),
|
||||
("render", Class::Layer(1)),
|
||||
("doc", Class::Layer(2)),
|
||||
@@ -62,7 +64,7 @@ pub const TABLE: &[(&str, Class)] = &[
|
||||
/// Explicit orderings *within* a layer (earlier may be used by later).
|
||||
/// The viewport draws display plans the GPU crate evaluates and draws with its pipelines; the MCP
|
||||
/// server's tests draw screenshots with them.
|
||||
pub const INTRA_LAYER_ORDER: &[&[&str]] = &[&["gpu", "ui-egui"], &["gpu", "mcp"]];
|
||||
pub const INTRA_LAYER_ORDER: &[&[&str]] = &[&["gpu", "ui-egui"], &["gpu", "mcp"], &["smlib", "kernel"]];
|
||||
|
||||
fn intra_layer_allowed(from: &str, to: &str) -> bool {
|
||||
let (from, to) = (short_name(from), short_name(to));
|
||||
|
||||
Reference in New Issue
Block a user