From 059824ff5a718ea17d159dd2cb3204e1882e3e3e Mon Sep 17 00:00:00 2001 From: Bruno Deferrari Date: Tue, 26 Dec 2023 11:49:57 -0300 Subject: [PATCH] WIP: threading support --- .github/workflows/build.yml | 2 +- Cargo.toml | 4 +- src/boxroot.rs | 10 ++- src/conv/to_ocaml.rs | 2 +- src/lib.rs | 2 +- src/runtime.rs | 70 +++++++++++++++++- src/value.rs | 2 +- testing/rust-caller/Cargo.toml | 2 +- testing/rust-caller/build.rs | 14 ++-- testing/rust-caller/ocaml/dune | 2 + testing/rust-caller/src/lib.rs | 130 +++++++++++++++++---------------- 11 files changed, 162 insertions(+), 78 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b554fe7..4fb9268 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: - name: Add Opam switch to PATH run: opam var bin >> $GITHUB_PATH - name: Setup Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@v1 with: toolchain: stable - name: Setttings for cargo in OSX diff --git a/Cargo.toml b/Cargo.toml index 89d6068..5b45e9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,11 @@ exclude = [ ] [package.metadata.docs.rs] -features = [ "without-ocamlopt" ] +features = [ "without-ocamlopt", "caml-state" ] [dependencies] ocaml-sys = "0.23" -ocaml-boxroot-sys = "0.3.0" +ocaml-boxroot-sys = { git = "https://gitlab.com/ocaml-rust/ocaml-boxroot.git", rev="652d92b1" } static_assertions = "1.1.0" [features] diff --git a/src/boxroot.rs b/src/boxroot.rs index 303155d..b3b2d99 100644 --- a/src/boxroot.rs +++ b/src/boxroot.rs @@ -46,7 +46,15 @@ impl BoxRoot { pub fn keep<'tmp>(&'tmp mut self, val: OCaml) -> OCamlRef<'tmp, T> { unsafe { if !boxroot_modify(&mut self.boxroot, val.raw) { - panic!("boxrooot_modify failed"); + let status = boxroot_status(); + let reason = match status { + Status::NotSetup => "NotSetup", + Status::Running => "Running", + Status::ToreDown => "ToreDown", + Status::Invalid => "Invalid", + _ => "Unknown", + }; + panic!("Failed to modify boxroot, boxroot_status() -> {}", reason,) } &*(boxroot_get_ref(self.boxroot) as *const OCamlCell) } diff --git a/src/conv/to_ocaml.rs b/src/conv/to_ocaml.rs index 4dd5ef2..92d769c 100644 --- a/src/conv/to_ocaml.rs +++ b/src/conv/to_ocaml.rs @@ -198,7 +198,7 @@ where A: ToOCaml, { fn to_ocaml<'a>(&self, cr: &'a mut OCamlRuntime) -> OCaml<'a, OCamlList> { - let mut result = BoxRoot::new(OCaml::nil()); + let mut result = BoxRoot::new(OCaml::nil(cr)); for elt in self.iter().rev() { let ov = elt.to_boxroot(cr); let cons = alloc_cons(cr, &ov, &result); diff --git a/src/lib.rs b/src/lib.rs index 054bf94..9771a22 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -302,7 +302,7 @@ pub use crate::memory::OCamlRef; pub use crate::mlvalues::{ bigarray, DynBox, OCamlBytes, OCamlFloat, OCamlInt, OCamlInt32, OCamlInt64, OCamlList, RawOCaml, }; -pub use crate::runtime::OCamlRuntime; +pub use crate::runtime::{OCamlRuntime, OCamlDomainLock}; pub use crate::value::OCaml; #[doc(hidden)] diff --git a/src/runtime.rs b/src/runtime.rs index e7eefa8..d429993 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -2,7 +2,10 @@ // SPDX-License-Identifier: MIT use ocaml_boxroot_sys::boxroot_teardown; -use std::marker::PhantomData; +use std::{ + marker::PhantomData, + ops::{Deref, DerefMut}, +}; use crate::{memory::OCamlRef, value::OCaml}; @@ -35,9 +38,10 @@ impl OCamlRuntime { INIT.call_once(|| { let arg0 = "ocaml\0".as_ptr() as *const ocaml_sys::Char; - let c_args = vec![arg0, core::ptr::null()]; + let c_args = [arg0, core::ptr::null()]; unsafe { ocaml_sys::caml_startup(c_args.as_ptr()); + ocaml_boxroot_sys::boxroot_setup_systhreads(); } }) } @@ -75,6 +79,10 @@ impl OCamlRuntime { raw: unsafe { reference.get_raw() }, } } + + pub fn acquire_lock() -> OCamlDomainLock { + OCamlDomainLock::new() + } } impl Drop for OCamlRuntime { @@ -108,6 +116,64 @@ impl Drop for OCamlBlockingSection { } } +pub struct OCamlDomainLock { + _private: (), +} + +extern "C" { + pub fn caml_c_thread_register() -> isize; + pub fn caml_c_thread_unregister() -> isize; +} + +impl OCamlDomainLock { + #[inline(always)] + fn new() -> Self { + unsafe { + caml_c_thread_register(); + ocaml_sys::caml_leave_blocking_section(); + }; + Self { _private: () } + } + + #[inline(always)] + pub fn perform(self, f: F) -> T + where + F: FnOnce(&mut OCamlRuntime) -> T, + { + let cr = unsafe { OCamlRuntime::recover_handle() }; + f(cr) + } + + // FIXME: immutable reference but gets mut runtime + #[inline(always)] + pub fn recover_handle<'a>(&self) -> &'a mut OCamlRuntime { + unsafe { OCamlRuntime::recover_handle() } + } +} + +impl Drop for OCamlDomainLock { + fn drop(&mut self) { + unsafe { + ocaml_sys::caml_enter_blocking_section(); + caml_c_thread_unregister(); + }; + } +} + +impl Deref for OCamlDomainLock { + type Target = OCamlRuntime; + + fn deref(&self) -> &OCamlRuntime { + self.recover_handle() + } +} + +impl DerefMut for OCamlDomainLock { + fn deref_mut(&mut self) -> &mut OCamlRuntime { + self.recover_handle() + } +} + // For initializing from an OCaml-driven program #[no_mangle] diff --git a/src/value.rs b/src/value.rs index 3507ec2..cf33891 100644 --- a/src/value.rs +++ b/src/value.rs @@ -369,7 +369,7 @@ impl<'a, A, Err> OCaml<'a, Result> { impl<'a, A> OCaml<'a, OCamlList> { /// Returns an OCaml nil (empty list) value. - pub fn nil() -> Self { + pub fn nil(_: &'a mut OCamlRuntime) -> Self { OCaml { _marker: PhantomData, raw: EMPTY_LIST, diff --git a/testing/rust-caller/Cargo.toml b/testing/rust-caller/Cargo.toml index 453d686..905e882 100644 --- a/testing/rust-caller/Cargo.toml +++ b/testing/rust-caller/Cargo.toml @@ -9,4 +9,4 @@ ocaml-interop = { path = "../.." } ocaml-sys = "*" [dev-dependencies] -serial_test = "*" \ No newline at end of file +once_cell = "*" diff --git a/testing/rust-caller/build.rs b/testing/rust-caller/build.rs index 2825de9..93a54ae 100644 --- a/testing/rust-caller/build.rs +++ b/testing/rust-caller/build.rs @@ -8,29 +8,29 @@ fn main() { let ocaml_callable_dir = "./ocaml"; let dune_dir = "../../_build/default/testing/rust-caller/ocaml"; Command::new("opam") - .args(&["exec", "--", "dune", "build", &format!("{}/callable.exe.o", ocaml_callable_dir)]) + .args(["exec", "--", "dune", "build", &format!("{}/callable.exe.o", ocaml_callable_dir)]) .status() .expect("Dune failed"); Command::new("rm") - .args(&["-f", &format!("{}/libcallable.a", out_dir)]) + .args(["-f", &format!("{}/libcallable.a", out_dir)]) .status() .expect("rm failed"); Command::new("rm") - .args(&["-f", &format!("{}/libcallable.o", out_dir)]) + .args(["-f", &format!("{}/libcallable.o", out_dir)]) .status() .expect("rm failed"); Command::new("cp") - .args(&[ + .args([ &format!("{}/callable.exe.o", dune_dir), - &format!("{}/libcallable.o", out_dir), + &format!("{}/callable.o", out_dir), ]) .status() .expect("File copy failed."); Command::new("ar") - .args(&[ + .args([ "qs", &format!("{}/libcallable.a", out_dir), - &format!("{}/libcallable.o", out_dir), + &format!("{}/callable.o", out_dir), ]) .status() .expect("ar failed"); diff --git a/testing/rust-caller/ocaml/dune b/testing/rust-caller/ocaml/dune index d047731..737f7af 100644 --- a/testing/rust-caller/ocaml/dune +++ b/testing/rust-caller/ocaml/dune @@ -1,3 +1,5 @@ (executables (names callable) + (libraries threads) + (flags (:standard -noautolink -cclib -lunix -cclib -lthreadsnat)) (modes object)) \ No newline at end of file diff --git a/testing/rust-caller/src/lib.rs b/testing/rust-caller/src/lib.rs index 66fce6e..768fd77 100644 --- a/testing/rust-caller/src/lib.rs +++ b/testing/rust-caller/src/lib.rs @@ -4,7 +4,7 @@ extern crate ocaml_interop; #[cfg(test)] -use ocaml_interop::cons; +use ocaml_interop::{cons, OCamlDomainLock}; use ocaml_interop::{OCaml, OCamlBytes, OCamlRuntime, ToOCaml}; #[cfg(test)] use std::borrow::Borrow; @@ -140,7 +140,10 @@ pub fn verify_variant_test(cr: &mut OCamlRuntime, variant: ocaml::Movement) -> S result.to_rust(cr) } -pub fn verify_polymorphic_variant_test(cr: &mut OCamlRuntime, variant: ocaml::PolymorphicEnum) -> String { +pub fn verify_polymorphic_variant_test( + cr: &mut OCamlRuntime, + variant: ocaml::PolymorphicEnum, +) -> String { let ocaml_variant = variant.to_boxroot(cr); let result = ocaml::stringify_polymorphic_variant(cr, &ocaml_variant); result.to_rust(cr) @@ -158,29 +161,27 @@ pub fn allocate_alot(cr: &mut OCamlRuntime) -> bool { // Tests -// NOTE: required because at the moment, no synchronization is done on OCaml calls #[cfg(test)] -use serial_test::serial; +fn acquire_domain_lock() -> OCamlDomainLock { + static INIT: std::sync::Once = std::sync::Once::new(); -// TODO: add a mutex here, initialize the runtime once, handle panics to avoid poisoning, etc -#[cfg(test)] -unsafe fn acquire_runtime_handle<'a>() -> &'static mut OCamlRuntime{ - OCamlRuntime::init_persistent(); - ocaml_sys::caml_leave_blocking_section(); - OCamlRuntime::recover_handle() + INIT.call_once(|| { + OCamlRuntime::init_persistent(); + unsafe { ocaml_sys::caml_enter_blocking_section() }; + }); + + OCamlRuntime::acquire_lock() } #[test] -#[serial] fn test_twice() { - let mut cr = unsafe { acquire_runtime_handle() }; + let mut cr = acquire_domain_lock(); assert_eq!(twice(&mut cr, 10), 20); } #[test] -#[serial] fn test_increment_bytes() { - let mut cr = unsafe { acquire_runtime_handle() }; + let mut cr = acquire_domain_lock(); assert_eq!( increment_bytes(&mut cr, "0000000000000000", 10), "1111111111000000" @@ -188,18 +189,16 @@ fn test_increment_bytes() { } #[test] -#[serial] fn test_increment_ints_list() { - let mut cr = unsafe { acquire_runtime_handle() }; + let mut cr = acquire_domain_lock(); let ints = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; let expected = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; assert_eq!(increment_ints_list(&mut cr, &ints), expected); } #[test] -#[serial] fn test_make_tuple() { - let mut cr = unsafe { acquire_runtime_handle() }; + let mut cr = acquire_domain_lock(); assert_eq!( make_tuple(&mut cr, "fst".to_owned(), 9), ("fst".to_owned(), 9) @@ -207,9 +206,8 @@ fn test_make_tuple() { } #[test] -#[serial] fn test_make_some() { - let mut cr = unsafe { acquire_runtime_handle() }; + let mut cr = acquire_domain_lock(); assert_eq!( make_some(&mut cr, "some".to_owned()), Some("some".to_owned()) @@ -217,9 +215,8 @@ fn test_make_some() { } #[test] -#[serial] fn test_make_result() { - let mut cr = unsafe { acquire_runtime_handle() }; + let mut cr = acquire_domain_lock(); assert_eq!(make_ok(&mut cr, 10), Ok(10)); assert_eq!( make_error(&mut cr, "error".to_owned()), @@ -228,16 +225,14 @@ fn test_make_result() { } #[test] -#[serial] fn test_frame_management() { - let mut cr = unsafe { acquire_runtime_handle() }; - assert_eq!(allocate_alot(&mut cr), true); + let mut cr = acquire_domain_lock(); + assert!(allocate_alot(&mut cr)); } #[test] -#[serial] fn test_record_conversion() { - let mut cr = unsafe { acquire_runtime_handle() }; + let mut cr = acquire_domain_lock(); let record = ocaml::TestRecord { i: 10, f: 5.0, @@ -251,9 +246,8 @@ fn test_record_conversion() { } #[test] -#[serial] fn test_variant_conversion() { - let mut cr = unsafe { acquire_runtime_handle() }; + let mut cr = acquire_domain_lock(); assert_eq!( verify_variant_test(&mut cr, ocaml::Movement::RotateLeft), "RotateLeft".to_owned() @@ -269,9 +263,8 @@ fn test_variant_conversion() { } #[test] -#[serial] fn test_polymorphic_variant_conversion() { - let mut cr = unsafe { acquire_runtime_handle() }; + let mut cr = acquire_domain_lock(); assert_eq!( verify_polymorphic_variant_test(&mut cr, ocaml::PolymorphicEnum::Unit), "Unit".to_owned() @@ -281,86 +274,78 @@ fn test_polymorphic_variant_conversion() { "Single(10.00)".to_owned() ); assert_eq!( - verify_polymorphic_variant_test(&mut cr, ocaml::PolymorphicEnum::Multiple(10, "text".to_string())), + verify_polymorphic_variant_test( + &mut cr, + ocaml::PolymorphicEnum::Multiple(10, "text".to_string()) + ), "Multiple(10, text)".to_owned() ); } #[test] -#[serial] fn test_bigarray() { - let mut cr = unsafe { acquire_runtime_handle() }; - + let mut cr = acquire_domain_lock(); let arr: Vec = (0..16).collect(); - let crr = &mut cr; - let arr_ocaml: BoxRoot> = arr.as_slice().to_boxroot(crr); - ocaml::double_u16_array(crr, &arr_ocaml); + let arr_ocaml: BoxRoot> = arr.as_slice().to_boxroot(&mut cr); + ocaml::double_u16_array(&mut cr, &arr_ocaml); assert_eq!( - crr.get(&arr_ocaml).as_slice(), + cr.get(&arr_ocaml).as_slice(), (0..16u16).map(|i| i * 2).collect::>().as_slice() ); } #[test] -#[serial] fn test_exception_handling_with_message() { - OCamlRuntime::init_persistent(); + let mut cr = acquire_domain_lock(); let result = std::panic::catch_unwind(move || { - let mut cr = unsafe { acquire_runtime_handle() }; - let mcr = &mut cr; - let message = "my-error-message".to_boxroot(mcr); - ocaml::raises_message_exception(mcr, &message); + let message = "my-error-message".to_boxroot(&mut cr); + ocaml::raises_message_exception(&mut cr, &message); }); assert_eq!( result .err() - .and_then(|err| Some(err.downcast_ref::().unwrap().clone())) + .map(|err| err.downcast_ref::().unwrap().clone()) .unwrap(), "OCaml exception, message: Some(\"my-error-message\")" ); } #[test] -#[serial] fn test_exception_handling_without_message() { - OCamlRuntime::init_persistent(); - let result = std::panic::catch_unwind(|| { - let cr = unsafe { OCamlRuntime::recover_handle() }; - ocaml::raises_nonmessage_exception(cr, &OCaml::unit()); + let mut cr = acquire_domain_lock(); + let result = std::panic::catch_unwind(move || { + ocaml::raises_nonmessage_exception(&mut cr, &OCaml::unit()); }); assert_eq!( result .err() - .and_then(|err| Some(err.downcast_ref::().unwrap().clone())) + .map(|err| err.downcast_ref::().unwrap().clone()) .unwrap(), "OCaml exception, message: None" ); } #[test] -#[serial] fn test_exception_handling_nonblock_exception() { - OCamlRuntime::init_persistent(); - let result = std::panic::catch_unwind(|| { - let cr = unsafe { OCamlRuntime::recover_handle() }; - ocaml::raises_nonblock_exception(cr, &OCaml::unit()); + let mut cr = acquire_domain_lock(); + let result = std::panic::catch_unwind(move || { + ocaml::raises_nonblock_exception(&mut cr, &OCaml::unit()); }); assert_eq!( result .err() - .and_then(|err| Some(err.downcast_ref::().unwrap().clone())) + .map(|err| err.downcast_ref::().unwrap().clone()) .unwrap(), "OCaml exception, message: None" ); } #[test] -#[serial] fn test_dynbox() { - let mut cr = unsafe { acquire_runtime_handle() }; + let mut cr = acquire_domain_lock(); - let mut list = OCaml::nil().root(); + let mut list = OCaml::nil(&mut cr).root(); let mut l2; // Note: building a list with cons will build it in reverse order for e in (0u16..4).rev() { @@ -377,3 +362,26 @@ fn test_dynbox() { ocaml::gc_compact(&mut cr, OCaml::unit().as_ref()); assert_eq!(vec2, vec![3, 2, 1, 0]); } + +#[test] +fn test_threads() { + // Create a vector to store the handles of the spawned threads + let mut handles = Vec::new(); + + // Spawn 100 threads + for n in 0..100 { + let handle = std::thread::spawn(move || { + let mut cr = acquire_domain_lock(); + println!("thread: {n}"); + allocate_alot(&mut cr) + }); + + handles.push((n, handle)); + } + std::thread::sleep(std::time::Duration::from_secs(1)); + // Wait for all of the threads to finish + for (n, handle) in handles { + println!("Joining thread {n}"); + assert!(handle.is_finished()); + } +}