ocaml_interop/
runtime.rs

1// Copyright (c) Viable Systems and TezEdge Contributors
2// SPDX-License-Identifier: MIT
3
4use ocaml_boxroot_sys::boxroot_teardown;
5use std::{
6    cell::UnsafeCell,
7    marker::PhantomData,
8    ops::{Deref, DerefMut},
9};
10
11use crate::{memory::OCamlRef, value::OCaml};
12
13thread_local! {
14  static TLS_RUNTIME: UnsafeCell<OCamlRuntime> = const { UnsafeCell::new({
15      OCamlRuntime { _not_send_sync: PhantomData }
16  })};
17}
18
19/// RAII guard for the OCaml runtime.
20pub struct OCamlRuntimeStartupGuard {
21    _not_send_sync: PhantomData<*const ()>,
22}
23
24impl Deref for OCamlRuntimeStartupGuard {
25    type Target = OCamlRuntime;
26
27    fn deref(&self) -> &OCamlRuntime {
28        unsafe { internal::recover_runtime_handle() }
29    }
30}
31
32impl DerefMut for OCamlRuntimeStartupGuard {
33    fn deref_mut(&mut self) -> &mut OCamlRuntime {
34        unsafe { internal::recover_runtime_handle_mut() }
35    }
36}
37
38/// Per-thread handle to the OCaml runtime.
39///
40/// The first call to `OCamlRuntime::init()` on the “main” thread
41/// will perform `caml_startup` and initialize the runtime. The
42/// returned `OCamlRuntimeStartupGuard`, once dropped, will
43/// perform the OCaml runtime shutdown and release resources.
44///
45/// In normal use you don’t pass this handle around yourself—invoke
46/// `OCamlRuntime::with_domain_lock(...)` (or use the provided FFI
47/// export macros) to enter the OCaml domain and get a `&mut` to it.
48pub struct OCamlRuntime {
49    _not_send_sync: PhantomData<*const ()>,
50}
51
52impl OCamlRuntime {
53    /// Initialize the OCaml runtime exactly once.
54    ///
55    /// Returns a `OCamlRuntimeStartupGuard` that will perform the
56    /// OCaml runtime shutdown and release resources once dropped.
57    ///
58    /// Returns `Err(String)` if called more than once.
59    pub fn init() -> Result<OCamlRuntimeStartupGuard, String> {
60        #[cfg(not(feature = "no-caml-startup"))]
61        {
62            use std::sync::atomic::{AtomicBool, Ordering};
63
64            static INIT_CALLED: AtomicBool = AtomicBool::new(false);
65
66            if INIT_CALLED
67                .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
68                .is_err()
69            {
70                return Err("OCaml runtime already initialized".to_string());
71            }
72            unsafe {
73                let arg0 = c"ocaml".as_ptr() as *const ocaml_sys::Char;
74                let args = [arg0, core::ptr::null()];
75                ocaml_sys::caml_startup(args.as_ptr());
76                ocaml_boxroot_sys::boxroot_setup();
77                ocaml_sys::caml_enter_blocking_section();
78            }
79
80            Ok(OCamlRuntimeStartupGuard {
81                _not_send_sync: PhantomData,
82            })
83        }
84        #[cfg(feature = "no-caml-startup")]
85        return Err(
86            "Rust code called from OCaml should not try to initialize the runtime".to_string(),
87        );
88    }
89
90    /// Release the OCaml runtime lock, call `f`, and re-acquire the OCaml runtime lock.
91    pub fn releasing_runtime<T, F>(&mut self, f: F) -> T
92    where
93        F: FnOnce() -> T,
94    {
95        OCamlBlockingSection::new().perform(f)
96    }
97
98    /// Returns the OCaml valued to which this GC tracked reference points to.
99    pub fn get<'tmp, T>(&'tmp self, reference: OCamlRef<T>) -> OCaml<'tmp, T> {
100        OCaml {
101            _marker: PhantomData,
102            raw: unsafe { reference.get_raw() },
103        }
104    }
105
106    /// Run f with the OCaml lock held (enter / leave automatically).
107    ///
108    /// This is a blocking call that will wait until the OCaml runtime is available.
109    pub fn with_domain_lock<F, T>(f: F) -> T
110    where
111        F: FnOnce(&mut Self) -> T,
112    {
113        let mut lock = OCamlDomainLock::new();
114        f(&mut lock)
115    }
116}
117
118impl Drop for OCamlRuntimeStartupGuard {
119    fn drop(&mut self) {
120        unsafe {
121            ocaml_sys::caml_leave_blocking_section();
122            boxroot_teardown();
123            ocaml_sys::caml_shutdown();
124        }
125    }
126}
127
128struct OCamlBlockingSection;
129
130impl OCamlBlockingSection {
131    fn new() -> Self {
132        Self
133    }
134
135    fn perform<T, F>(self, f: F) -> T
136    where
137        F: FnOnce() -> T,
138    {
139        unsafe { ocaml_sys::caml_enter_blocking_section() };
140        f()
141    }
142}
143
144impl Drop for OCamlBlockingSection {
145    fn drop(&mut self) {
146        unsafe { ocaml_sys::caml_leave_blocking_section() };
147    }
148}
149
150/// In OCaml 5 each **domain** has its own minor-heap and GC state.  Entering
151/// OCaml from C requires the thread to *leave* the “blocking section”, thereby
152/// resuming normal allocation/GC activity for **this domain**.  This guard
153/// performs that transition in `new()` and restores the blocking section in
154/// `Drop`.
155///
156/// While the guard is alive the current OS thread is the **only** thread that
157/// can run OCaml code *inside this domain*.  That exclusivity makes it sound
158/// to hand out **one** mutable reference to the process-wide
159/// [`OCamlRuntime`] but only for the guard’s lifetime.
160///
161/// # Safety invariant
162///
163/// *Each* live `OCamlDomainLock` owns the “entered” state of the current
164/// domain.  Creating a second guard simultaneously (nesting) would yield two
165/// overlapping `&mut OCamlRuntime` borrows (that is undefined behaviour in
166/// Rust) and would also violate the enter/leave protocol required by the OCaml
167/// C API.  Likewise, leaking a guard with `mem::forget` keeps the domain
168/// permanently *entered* and the mutable reference alive beyond its intended
169/// scope; both are unsound.
170///
171/// Consequently this type is **!Send + !Sync** and must remain on the thread
172/// where it was constructed.
173struct OCamlDomainLock {
174    _not_send_sync: PhantomData<*const ()>,
175}
176
177impl OCamlDomainLock {
178    #[inline(always)]
179    fn new() -> Self {
180        OCamlThreadRegistrationGuard::ensure();
181        unsafe {
182            ocaml_sys::caml_leave_blocking_section();
183        };
184        Self {
185            _not_send_sync: PhantomData,
186        }
187    }
188}
189
190impl Drop for OCamlDomainLock {
191    fn drop(&mut self) {
192        unsafe {
193            ocaml_sys::caml_enter_blocking_section();
194        };
195    }
196}
197
198impl Deref for OCamlDomainLock {
199    type Target = OCamlRuntime;
200
201    fn deref(&self) -> &OCamlRuntime {
202        unsafe { internal::recover_runtime_handle() }
203    }
204}
205
206impl DerefMut for OCamlDomainLock {
207    fn deref_mut(&mut self) -> &mut OCamlRuntime {
208        unsafe { internal::recover_runtime_handle_mut() }
209    }
210}
211
212// Thread registration handling
213
214extern "C" {
215    pub fn caml_c_thread_register() -> isize;
216    pub fn caml_c_thread_unregister() -> isize;
217}
218
219/// RAII guard for per-thread OCaml runtime registration.
220///
221/// This struct is instantiated once per thread (via the `thread_local!`
222/// `OCAML_THREAD`) to ensure that the OCaml runtime is registered
223/// before any FFI calls into OCaml. The `registered` field is set to `true`
224/// **only** if the initial call to `caml_c_thread_register()` returns `1`
225/// (indicating success). When the thread exits, the guard’s `Drop`
226/// implementation will call `caml_c_thread_unregister()` exactly once
227/// if and only if `registered` is `true`.
228struct OCamlThreadRegistrationGuard {
229    registered: bool,
230}
231
232thread_local! {
233    static OCAML_THREAD_REGISTRATION_GUARD: OCamlThreadRegistrationGuard = {
234        let ok = unsafe { caml_c_thread_register() } == 1;
235        OCamlThreadRegistrationGuard { registered: ok }
236    };
237}
238
239impl OCamlThreadRegistrationGuard {
240    /// **Call this at the start of any function that may touch the OCaml runtime.**
241    ///
242    /// After the first invocation in the thread it’s just a cheap TLS lookup.
243    #[inline(always)]
244    pub fn ensure() {
245        OCAML_THREAD_REGISTRATION_GUARD.with(|_| {});
246    }
247}
248
249impl Drop for OCamlThreadRegistrationGuard {
250    fn drop(&mut self) {
251        if self.registered {
252            unsafe {
253                caml_c_thread_unregister();
254            }
255        }
256    }
257}
258
259// For initializing from an OCaml-driven program
260
261#[no_mangle]
262extern "C" fn ocaml_interop_setup(_unit: crate::RawOCaml) -> crate::RawOCaml {
263    ocaml_sys::UNIT
264}
265
266#[no_mangle]
267extern "C" fn ocaml_interop_teardown(_unit: crate::RawOCaml) -> crate::RawOCaml {
268    unsafe { boxroot_teardown() };
269    ocaml_sys::UNIT
270}
271
272#[doc(hidden)]
273pub mod internal {
274    use super::{OCamlRuntime, TLS_RUNTIME};
275
276    #[inline(always)]
277    pub unsafe fn recover_runtime_handle_mut() -> &'static mut OCamlRuntime {
278        TLS_RUNTIME.with(|cell| &mut *cell.get())
279    }
280
281    #[inline(always)]
282    pub unsafe fn recover_runtime_handle() -> &'static OCamlRuntime {
283        TLS_RUNTIME.with(|cell| &*cell.get())
284    }
285}