diff --git a/Cargo.lock b/Cargo.lock index 485132769..d7190d0d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -631,12 +631,11 @@ dependencies = [ "figment", "fj", "fj-debug", + "fj-host", "fj-math", "fj-operations", "futures", - "libloading", "nalgebra", - "notify", "parry3d-f64", "serde", "thiserror", @@ -655,6 +654,16 @@ dependencies = [ "parry3d-f64", ] +[[package]] +name = "fj-host" +version = "0.5.0" +dependencies = [ + "fj", + "libloading", + "notify", + "thiserror", +] + [[package]] name = "fj-kernel" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index b0c38349e..069b25487 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "fj", "fj-app", "fj-debug", + "fj-host", "fj-kernel", "fj-math", "fj-operations", @@ -18,6 +19,7 @@ members = [ default-members = [ "fj-app", "fj-debug", + "fj-host", "fj-kernel", "fj-math", "fj-operations", diff --git a/fj-app/Cargo.toml b/fj-app/Cargo.toml index 51a6df9fe..1b9da326b 100644 --- a/fj-app/Cargo.toml +++ b/fj-app/Cargo.toml @@ -15,9 +15,7 @@ categories = ["mathematics", "rendering"] anyhow = "1.0.56" bytemuck = "1.8.0" futures = "0.3.21" -libloading = "0.7.2" nalgebra = "0.30.0" -notify = "5.0.0-pre.14" parry3d-f64 = "0.8.0" thiserror = "1.0.30" threemf = "0.2.0" @@ -42,6 +40,10 @@ path = "../fj" version = "0.5.0" path = "../fj-debug" +[dependencies.fj-host] +version = "0.5.0" +path = "../fj-host" + [dependencies.fj-math] version = "0.5.0" path = "../fj-math" diff --git a/fj-app/src/main.rs b/fj-app/src/main.rs index 4ccf11734..cfbf2aee7 100644 --- a/fj-app/src/main.rs +++ b/fj-app/src/main.rs @@ -4,13 +4,13 @@ mod config; mod graphics; mod input; mod mesh; -mod model; mod window; use std::path::PathBuf; use std::{collections::HashMap, time::Instant}; use fj_debug::DebugInfo; +use fj_host::Model; use fj_math::{Aabb, Scalar, Triangle}; use fj_operations::ToShape as _; use futures::executor::block_on; @@ -28,7 +28,6 @@ use crate::{ config::Config, graphics::{DrawConfig, Renderer}, mesh::MeshMaker, - model::Model, window::Window, }; diff --git a/fj-app/src/model.rs b/fj-app/src/model.rs deleted file mode 100644 index acba8d608..000000000 --- a/fj-app/src/model.rs +++ /dev/null @@ -1,235 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - ffi::OsStr, - io, - path::PathBuf, - process::Command, - sync::mpsc, - thread, -}; - -use notify::Watcher as _; -use thiserror::Error; - -pub struct Model { - src_path: PathBuf, - lib_path: PathBuf, - manifest_path: PathBuf, -} - -impl Model { - pub fn from_path( - path: PathBuf, - target_dir: Option, - ) -> io::Result { - let name = { - // Can't panic. It only would, if the path ends with "..", and we - // are canonicalizing it here to prevent that. - let canonical = path.canonicalize()?; - let file_name = canonical.file_name().unwrap(); - - file_name.to_string_lossy().replace('-', "_") - }; - - let src_path = path.join("src"); - - let lib_path = { - let file = if cfg!(windows) { - format!("{}.dll", name) - } else if cfg!(target_os = "macos") { - format!("lib{}.dylib", name) - } else { - //Unix - format!("lib{}.so", name) - }; - - let target_dir = target_dir.unwrap_or_else(|| path.join("target")); - target_dir.join("debug").join(file) - }; - - let manifest_path = path.join("Cargo.toml"); - - Ok(Self { - src_path, - lib_path, - manifest_path, - }) - } - - pub fn load_once( - &self, - arguments: &HashMap, - ) -> Result { - let manifest_path = self.manifest_path.display().to_string(); - - let status = Command::new("cargo") - .arg("build") - .args(["--manifest-path", &manifest_path]) - .status()?; - - if !status.success() { - return Err(Error::Compile); - } - - // So, strictly speaking this is all unsound: - // - `Library::new` requires us to abide by the arbitrary requirements - // of any library initialization or termination routines. - // - `Library::get` requires us to specify the correct type for the - // model function. - // - The model function itself is `unsafe`, because it is a function - // from across an FFI interface. - // - // Typical models won't have initialization or termination routines (I - // think), should abide by the `ModelFn` signature, and might not do - // anything unsafe. But we have no way to know that the library the user - // told us to load actually does (I think). - // - // I don't know of a way to fix this. We should take this as motivation - // to switch to a better technique: - // https://github.com/hannobraun/Fornjot/issues/71 - let shape = unsafe { - let lib = libloading::Library::new(&self.lib_path)?; - let model: libloading::Symbol = lib.get(b"model")?; - model(arguments) - }; - - Ok(shape) - } - - pub fn load_and_watch( - self, - parameters: HashMap, - ) -> Result { - let (tx, rx) = mpsc::sync_channel(0); - let tx2 = tx.clone(); - - let watch_path = self.src_path.clone(); - - let mut watcher = notify::recommended_watcher( - move |event: notify::Result| { - // Unfortunately the `notify` documentation doesn't say when - // this might happen, so no idea if it needs to be handled. - let event = event.expect("Error handling watch event"); - - // Various acceptable ModifyKind kinds. Varies across platforms - // (e.g. MacOs vs. Windows10) - if let notify::EventKind::Modify( - notify::event::ModifyKind::Any, - ) - | notify::EventKind::Modify( - notify::event::ModifyKind::Data( - notify::event::DataChange::Any, - ), - ) - | notify::EventKind::Modify( - notify::event::ModifyKind::Data( - notify::event::DataChange::Content, - ), - ) = event.kind - { - let file_ext = event - .paths - .get(0) - .expect("File path missing in watch event") - .extension(); - - let black_list = HashSet::from([ - OsStr::new("swp"), - OsStr::new("tmp"), - OsStr::new("swx"), - ]); - - if let Some(ext) = file_ext { - if black_list.contains(ext) { - return; - } - } - - // This will panic, if the other end is disconnected, which - // is probably the result of a panic on that thread, or the - // application is being shut down. - // - // Either way, not much we can do about it here, except - // maybe to provide a better error message in the future. - tx.send(()).unwrap(); - } - }, - )?; - - watcher.watch(&watch_path, notify::RecursiveMode::Recursive)?; - - // To prevent a race condition between the initial load and the start of - // watching, we'll trigger the initial load here, after having started - // watching. - // - // Will panic, if the receiving end has panicked. Not much we can do - // about that, if it happened. - thread::spawn(move || tx2.send(()).unwrap()); - - Ok(Watcher { - _watcher: Box::new(watcher), - channel: rx, - model: self, - parameters, - }) - } -} - -pub struct Watcher { - _watcher: Box, - channel: mpsc::Receiver<()>, - model: Model, - parameters: HashMap, -} - -impl Watcher { - pub fn receive(&self) -> Option { - match self.channel.try_recv() { - Ok(()) => { - let shape = match self.model.load_once(&self.parameters) { - Ok(shape) => shape, - Err(Error::Compile) => { - // It would be better to display an error in the UI, - // where the user can actually see it. Issue: - // https://github.com/hannobraun/fornjot/issues/30 - println!("Error compiling model"); - return None; - } - Err(err) => { - panic!("Error reloading model: {:?}", err); - } - }; - - Some(shape) - } - Err(mpsc::TryRecvError::Empty) => { - // Nothing to receive from the channel. - None - } - Err(mpsc::TryRecvError::Disconnected) => { - // The other end has disconnected. This is probably the result - // of a panic on the other thread, or a program shutdown in - // progress. In any case, not much we can do here. - panic!(); - } - } - } -} - -#[derive(Debug, Error)] -pub enum Error { - #[error("Error compiling model")] - Compile, - - #[error("I/O error while loading model")] - Io(#[from] io::Error), - - #[error("Error loading model from dynamic library")] - LibLoading(#[from] libloading::Error), - - #[error("Error watching model for changes")] - Notify(#[from] notify::Error), -} - -type ModelFn = - unsafe extern "C" fn(args: &HashMap) -> fj::Shape; diff --git a/fj-host/Cargo.toml b/fj-host/Cargo.toml index dabdfd716..f5f201c4d 100644 --- a/fj-host/Cargo.toml +++ b/fj-host/Cargo.toml @@ -4,6 +4,17 @@ version = "0.5.0" edition = "2021" description = "The world needs another CAD program." -readme = "README.md" +readme = "../README.md" repository = "https://github.com/hannobraun/fornjot" license = "0BSD" +keywords = ["cad", "programmatic", "code-cad"] + + +[dependencies] +libloading = "0.7.2" +notify = "5.0.0-pre.14" +thiserror = "1.0.30" + +[dependencies.fj] +version = "0.5.0" +path = "../fj" diff --git a/fj-host/README.md b/fj-host/README.md deleted file mode 100644 index 6ec8bb896..000000000 --- a/fj-host/README.md +++ /dev/null @@ -1 +0,0 @@ -`fj-host` has been renamed to `fj-app`. Please refer to the [`fj-app` crate](https://crates.io/crates/fj-app). diff --git a/fj-host/src/lib.rs b/fj-host/src/lib.rs index 28a7c3951..acba8d608 100644 --- a/fj-host/src/lib.rs +++ b/fj-host/src/lib.rs @@ -1,3 +1,235 @@ -compile_error!( - "`fj-host has been renamed to `fj-app`: https://crates.io/crates/fj-app" -); +use std::{ + collections::{HashMap, HashSet}, + ffi::OsStr, + io, + path::PathBuf, + process::Command, + sync::mpsc, + thread, +}; + +use notify::Watcher as _; +use thiserror::Error; + +pub struct Model { + src_path: PathBuf, + lib_path: PathBuf, + manifest_path: PathBuf, +} + +impl Model { + pub fn from_path( + path: PathBuf, + target_dir: Option, + ) -> io::Result { + let name = { + // Can't panic. It only would, if the path ends with "..", and we + // are canonicalizing it here to prevent that. + let canonical = path.canonicalize()?; + let file_name = canonical.file_name().unwrap(); + + file_name.to_string_lossy().replace('-', "_") + }; + + let src_path = path.join("src"); + + let lib_path = { + let file = if cfg!(windows) { + format!("{}.dll", name) + } else if cfg!(target_os = "macos") { + format!("lib{}.dylib", name) + } else { + //Unix + format!("lib{}.so", name) + }; + + let target_dir = target_dir.unwrap_or_else(|| path.join("target")); + target_dir.join("debug").join(file) + }; + + let manifest_path = path.join("Cargo.toml"); + + Ok(Self { + src_path, + lib_path, + manifest_path, + }) + } + + pub fn load_once( + &self, + arguments: &HashMap, + ) -> Result { + let manifest_path = self.manifest_path.display().to_string(); + + let status = Command::new("cargo") + .arg("build") + .args(["--manifest-path", &manifest_path]) + .status()?; + + if !status.success() { + return Err(Error::Compile); + } + + // So, strictly speaking this is all unsound: + // - `Library::new` requires us to abide by the arbitrary requirements + // of any library initialization or termination routines. + // - `Library::get` requires us to specify the correct type for the + // model function. + // - The model function itself is `unsafe`, because it is a function + // from across an FFI interface. + // + // Typical models won't have initialization or termination routines (I + // think), should abide by the `ModelFn` signature, and might not do + // anything unsafe. But we have no way to know that the library the user + // told us to load actually does (I think). + // + // I don't know of a way to fix this. We should take this as motivation + // to switch to a better technique: + // https://github.com/hannobraun/Fornjot/issues/71 + let shape = unsafe { + let lib = libloading::Library::new(&self.lib_path)?; + let model: libloading::Symbol = lib.get(b"model")?; + model(arguments) + }; + + Ok(shape) + } + + pub fn load_and_watch( + self, + parameters: HashMap, + ) -> Result { + let (tx, rx) = mpsc::sync_channel(0); + let tx2 = tx.clone(); + + let watch_path = self.src_path.clone(); + + let mut watcher = notify::recommended_watcher( + move |event: notify::Result| { + // Unfortunately the `notify` documentation doesn't say when + // this might happen, so no idea if it needs to be handled. + let event = event.expect("Error handling watch event"); + + // Various acceptable ModifyKind kinds. Varies across platforms + // (e.g. MacOs vs. Windows10) + if let notify::EventKind::Modify( + notify::event::ModifyKind::Any, + ) + | notify::EventKind::Modify( + notify::event::ModifyKind::Data( + notify::event::DataChange::Any, + ), + ) + | notify::EventKind::Modify( + notify::event::ModifyKind::Data( + notify::event::DataChange::Content, + ), + ) = event.kind + { + let file_ext = event + .paths + .get(0) + .expect("File path missing in watch event") + .extension(); + + let black_list = HashSet::from([ + OsStr::new("swp"), + OsStr::new("tmp"), + OsStr::new("swx"), + ]); + + if let Some(ext) = file_ext { + if black_list.contains(ext) { + return; + } + } + + // This will panic, if the other end is disconnected, which + // is probably the result of a panic on that thread, or the + // application is being shut down. + // + // Either way, not much we can do about it here, except + // maybe to provide a better error message in the future. + tx.send(()).unwrap(); + } + }, + )?; + + watcher.watch(&watch_path, notify::RecursiveMode::Recursive)?; + + // To prevent a race condition between the initial load and the start of + // watching, we'll trigger the initial load here, after having started + // watching. + // + // Will panic, if the receiving end has panicked. Not much we can do + // about that, if it happened. + thread::spawn(move || tx2.send(()).unwrap()); + + Ok(Watcher { + _watcher: Box::new(watcher), + channel: rx, + model: self, + parameters, + }) + } +} + +pub struct Watcher { + _watcher: Box, + channel: mpsc::Receiver<()>, + model: Model, + parameters: HashMap, +} + +impl Watcher { + pub fn receive(&self) -> Option { + match self.channel.try_recv() { + Ok(()) => { + let shape = match self.model.load_once(&self.parameters) { + Ok(shape) => shape, + Err(Error::Compile) => { + // It would be better to display an error in the UI, + // where the user can actually see it. Issue: + // https://github.com/hannobraun/fornjot/issues/30 + println!("Error compiling model"); + return None; + } + Err(err) => { + panic!("Error reloading model: {:?}", err); + } + }; + + Some(shape) + } + Err(mpsc::TryRecvError::Empty) => { + // Nothing to receive from the channel. + None + } + Err(mpsc::TryRecvError::Disconnected) => { + // The other end has disconnected. This is probably the result + // of a panic on the other thread, or a program shutdown in + // progress. In any case, not much we can do here. + panic!(); + } + } + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("Error compiling model")] + Compile, + + #[error("I/O error while loading model")] + Io(#[from] io::Error), + + #[error("Error loading model from dynamic library")] + LibLoading(#[from] libloading::Error), + + #[error("Error watching model for changes")] + Notify(#[from] notify::Error), +} + +type ModelFn = + unsafe extern "C" fn(args: &HashMap) -> fj::Shape;