libperl_rs/perl.rs
1//! `Perl` — RAII-managed wrapper around `*mut PerlInterpreter`.
2//!
3//! See `docs/plan/README.md` §3.4 for the design rationale (`NonNull` to
4//! encode the non-null invariant at the safe boundary while keeping
5//! pointer-style aliasing for the FFI layer).
6
7use std::env;
8use std::ffi::CString;
9use std::os::raw::{c_char, c_int};
10use std::ptr;
11use std::ptr::NonNull;
12
13use libperl_sys::{CV, PerlInterpreter, Perl_newXS, perl_alloc, perl_construct, perl_destruct, perl_parse};
14
15/// A live Perl interpreter. Allocated by `perl_alloc` and torn down by
16/// `perl_destruct` on drop.
17///
18/// The `my_perl` field is `NonNull<PerlInterpreter>` so that the
19/// "interpreter is never null" invariant is encoded in the type.
20/// FFI calls extract a raw pointer via [`Perl::as_ptr`] — that's the
21/// boundary where Rust's safe-typed world meets the C ABI.
22pub struct Perl {
23 my_perl: NonNull<PerlInterpreter>,
24 args: Vec<CString>,
25 env: Vec<CString>,
26}
27
28// `NonNull<T>` is automatically `!Send !Sync`, which matches the Perl
29// convention of "1 interpreter = 1 thread". No `unsafe impl Send/Sync`
30// is provided.
31
32impl Perl {
33 /// Allocate and construct a fresh interpreter. Panics on allocation
34 /// failure (typically OOM, very rare).
35 pub fn new() -> Self {
36 let raw = unsafe { perl_alloc() };
37 let my_perl = NonNull::new(raw)
38 .expect("perl_alloc returned null (out of memory?)");
39 unsafe { perl_construct(my_perl.as_ptr()) };
40 Perl {
41 my_perl,
42 args: Vec::new(),
43 env: Vec::new(),
44 }
45 }
46
47 /// Raw pointer for FFI. The conventional name is `my_perl` at the
48 /// call site — see `docs/plan/README.md` §3.8 for naming rules.
49 #[inline]
50 pub fn as_ptr(&self) -> *mut PerlInterpreter {
51 self.my_perl.as_ptr()
52 }
53
54 /// Wrap a raw `*mut PerlInterpreter` as a borrowed `Perl`.
55 ///
56 /// # Safety
57 /// - `p` must point to a live, valid interpreter.
58 /// - The returned `Perl` MUST NOT be dropped: its `Drop` runs
59 /// `perl_destruct`, tearing down an interpreter this constructor
60 /// does not own. The intended usage is to wrap in
61 /// `core::mem::ManuallyDrop` immediately. The `#[xs_sub]`
62 /// proc-macro does this when a body declares a `my_perl: &Perl`
63 /// first parameter.
64 pub unsafe fn from_raw_unchecked(p: *mut PerlInterpreter) -> Self {
65 // Non-threaded builds: the `#[xs_sub]` proc-macro passes a
66 // null `my_perl` stub here. That's fine — every FFI call goes
67 // through `thx_call!`, which in non-threaded mode discards
68 // the `Perl` argument before invoking the bare libperl-sys
69 // function. So `as_ptr()` is never actually dereferenced;
70 // a dangling sentinel works as a placeholder.
71 //
72 // Threaded builds: a null pointer here is a programming
73 // error (callers must hand over a live interpreter). Callers
74 // get the same `dangling()` sentinel rather than a panic, so
75 // the failure surfaces at the first FFI deref instead of at
76 // construction — matches the rest of `unsafe`'s "garbage in,
77 // segfault out" contract.
78 let my_perl = NonNull::new(p).unwrap_or(NonNull::dangling());
79 Perl {
80 my_perl,
81 args: Vec::new(),
82 env: Vec::new(),
83 }
84 }
85
86 /// `perl_parse` with an explicit args / envp slice.
87 pub fn parse<S: AsRef<str>>(&mut self, args: &[S], envp: &[S]) -> i32 {
88 self.args = args
89 .iter()
90 .map(|a| CString::new(a.as_ref()).unwrap())
91 .collect();
92 self.env = envp
93 .iter()
94 .map(|a| CString::new(a.as_ref()).unwrap())
95 .collect();
96 self.perl_parse_inner()
97 }
98
99 /// `perl_parse` driven from `std::env::args()` / `vars()`.
100 pub fn parse_env_args(&mut self, args: env::Args, envp: env::Vars) -> i32 {
101 self.args = args
102 .map(|a| CString::new(a).unwrap())
103 .collect();
104 self.env = envp
105 .map(|(k, v)| CString::new(format!("{k}={v}")).unwrap())
106 .collect();
107 self.perl_parse_inner()
108 }
109
110 fn perl_parse_inner(&mut self) -> i32 {
111 unsafe {
112 perl_parse(
113 self.as_ptr(),
114 Some(xs_init as XsInitFn),
115 self.args.len() as c_int,
116 make_argv(&self.args).as_ptr() as *mut *mut c_char,
117 ensure_terminating_null(make_argv(&self.env)).as_ptr() as *mut *mut c_char,
118 )
119 }
120 }
121}
122
123impl Default for Perl {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129impl Drop for Perl {
130 fn drop(&mut self) {
131 unsafe { perl_destruct(self.as_ptr()) };
132 }
133}
134
135// ─── xs_init / DynaLoader bootstrap ────────────────────────────────
136
137unsafe extern "C" {
138 #[cfg(perl_useithreads)]
139 fn boot_DynaLoader(perl: *mut PerlInterpreter, cv: *mut CV);
140 #[cfg(not(perl_useithreads))]
141 fn boot_DynaLoader(cv: *mut CV);
142}
143
144#[cfg(perl_useithreads)]
145type XsInitFn = extern "C" fn(*mut PerlInterpreter);
146#[cfg(not(perl_useithreads))]
147type XsInitFn = extern "C" fn();
148
149#[cfg(perl_useithreads)]
150extern "C" fn xs_init(my_perl: *mut PerlInterpreter) {
151 let name = c"DynaLoader::boot_DynaLoader".as_ptr();
152 let file = c"libperl-rs".as_ptr();
153 unsafe { Perl_newXS(my_perl, name, Some(boot_DynaLoader), file) };
154}
155
156#[cfg(not(perl_useithreads))]
157extern "C" fn xs_init() {
158 let name = c"DynaLoader::boot_DynaLoader".as_ptr();
159 let file = c"libperl-rs".as_ptr();
160 unsafe { Perl_newXS(name, Some(boot_DynaLoader), file) };
161}
162
163// ─── small argv helpers ────────────────────────────────────────────
164
165fn make_argv(args: &[CString]) -> Vec<*mut c_char> {
166 args.iter().map(|a| a.as_ptr() as *mut c_char).collect()
167}
168
169fn ensure_terminating_null(mut argv: Vec<*mut c_char>) -> Vec<*mut c_char> {
170 if argv.last().is_none_or(|p| !p.is_null()) {
171 argv.push(ptr::null_mut());
172 }
173 argv
174}
175
176// ─── perl_call! macro ──────────────────────────────────────────────
177
178/// Wrap a `Perl_*` (bindgen) function call so the source compiles
179/// against both threaded and non-threaded Perl without `cfg`.
180///
181/// In threaded builds, `$my_perl` is prepended as the first argument.
182/// In non-threaded builds, `$my_perl` is type-checked, evaluated once,
183/// and discarded.
184///
185/// ```ignore
186/// let my_perl = perl.as_ptr();
187/// let cv = perl_call!(my_perl, Perl_newXS(name.as_ptr(), sub, file.as_ptr()));
188/// ```
189///
190/// (See `docs/plan/README.md` §3.6 for the argument-form rationale and
191/// hygiene constraints that prevent a no-arg variant.)
192#[cfg(perl_useithreads)]
193#[macro_export]
194macro_rules! perl_call {
195 ($my_perl:expr, $f:ident ( $($arg:expr),* $(,)? )) => {{
196 let __my_perl: *mut $crate::PerlInterpreter = $my_perl;
197 unsafe { $crate::$f(__my_perl, $($arg),*) }
198 }};
199}
200
201#[cfg(not(perl_useithreads))]
202#[macro_export]
203macro_rules! perl_call {
204 ($my_perl:expr, $f:ident ( $($arg:expr),* $(,)? )) => {{
205 // type-check + evaluate-once for source portability with the
206 // threaded form, then discard in non-threaded
207 let _: *mut $crate::PerlInterpreter = $my_perl;
208 unsafe { $crate::$f($($arg),*) }
209 }};
210}