solidcraft-smlib: read SMLib's log lines inside the FFI module

ffi::line was a safe public function reading any raw pointer. The log
callback now lives in ffi.rs, reads the C string there, and hands the
crate a String through a safe sink, so no safe function takes a pointer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
This commit is contained in:
jyviko
2026-10-21 00:34:55 -04:00
parent 60aa5e697c
commit 3c0b539356
2 changed files with 25 additions and 10 deletions
+23 -7
View File
@@ -20,6 +20,7 @@ pub struct RawBrep {
_private: [u8; 0],
}
#[cfg(feature = "native")]
pub type LogFn = extern "C" fn(user: *mut c_void, level: i32, line: *const c_char);
#[cfg(feature = "native")]
@@ -88,7 +89,16 @@ mod calls {
pub fn init() -> i32 {
unsafe { smlib_init() }
}
pub fn set_log(log: Option<LogFn>) {
/// Routes SMLib's log lines to `sink` (`None`: stops them).
pub fn set_log(sink: Option<fn(i32, String)>) {
let log: Option<LogFn> = match sink {
Some(sink) => {
// The first sink stays: init sets it once, and lines keep a single destination.
let _ = SINK.set(sink);
Some(trampoline)
}
None => None,
};
unsafe { smlib_set_log(log, std::ptr::null_mut()) }
}
pub fn set_dump_dir(dir: Option<&CStr>) -> i32 {
@@ -139,7 +149,7 @@ mod calls {
pub fn init() -> i32 {
NOT_BUILT
}
pub fn set_log(_: Option<LogFn>) {}
pub fn set_log(_: Option<fn(i32, String)>) {}
pub fn set_dump_dir(_: Option<&CStr>) -> i32 {
NOT_BUILT
}
@@ -168,10 +178,16 @@ mod calls {
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();
/// Where [`set_log`] sends SMLib's log lines.
#[cfg(feature = "native")]
static SINK: std::sync::OnceLock<fn(i32, String)> = std::sync::OnceLock::new();
/// The log callback the C API calls: `line` is NUL-terminated and valid for the call's duration
/// (smlib.h), so it is read here, inside the one module that touches raw pointers.
#[cfg(feature = "native")]
extern "C" fn trampoline(_: *mut c_void, level: i32, line: *const c_char) {
let text = if line.is_null() { String::new() } else { unsafe { std::ffi::CStr::from_ptr(line) }.to_string_lossy().into_owned() };
if let Some(sink) = SINK.get() {
sink(level, text);
}
unsafe { std::ffi::CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
+2 -3
View File
@@ -10,7 +10,7 @@
//! [`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::ffi::CString;
use std::path::Path;
use std::sync::Once;
use std::sync::atomic::{AtomicI32, Ordering};
@@ -70,8 +70,7 @@ 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);
fn log_line(level: i32, text: String) {
if level == 0 {
log::debug!(target: "smlib", "{text}");
} else {