Skip to main content

jit_lang/
compiled.rs

1//! The compiled, runnable function: machine code in executable memory and the
2//! metadata needed to call it.
3
4use std::mem;
5
6use ir_lang::Type;
7use pager_lang::Region;
8
9/// A compiled function: native machine code living in executable memory, ready to run.
10///
11/// A [`Jit`](crate::Jit) produces one of these by lowering an
12/// [`ir_lang::Function`](ir_lang::Function) to host machine code and placing it in a
13/// page-aligned, read-execute [`Region`]. `Compiled` owns that region, so the code
14/// stays mapped and runnable for exactly as long as the value is alive and is freed
15/// when it is dropped — calling into a function whose `Compiled` has been dropped is a
16/// use-after-free.
17///
18/// The signature accessors — [`name`](Self::name), [`params`](Self::params), and
19/// [`ret`](Self::ret) — report the function as it was compiled, so a caller can pick
20/// the right function-pointer type before calling. Running the code is inherently
21/// unsafe: the machine has no way to check that the type you transmute the entry point
22/// to matches what was compiled, so that final step is your responsibility through
23/// [`entry`](Self::entry). See its documentation for the contract.
24///
25/// `Compiled` is [`Send`] and [`Sync`]: the region it owns is, and the rest of its
26/// fields are plain data. Moving it between threads moves the mapping with it.
27///
28/// # Examples
29///
30/// Compile a function and read back what it is:
31///
32/// ```
33/// use jit_lang::compile;
34/// use ir_lang::{Builder, BinOp, Type};
35///
36/// // fn double(x: int) -> int { x + x }
37/// let mut b = Builder::new("double", &[Type::Int], Type::Int);
38/// let x = b.block_params(b.entry())[0];
39/// let sum = b.bin(BinOp::Add, x, x);
40/// b.ret(Some(sum));
41///
42/// let f = compile(&b.finish()).expect("double is well-formed");
43/// assert_eq!(f.name(), "double");
44/// assert_eq!(f.params(), &[Type::Int]);
45/// assert_eq!(f.ret(), Type::Int);
46/// assert!(f.code_len() > 0);
47/// ```
48pub struct Compiled {
49    /// The executable memory holding the code. Owned so the mapping outlives every
50    /// call and is freed on drop.
51    region: Region,
52    /// The compiled function's name, carried for inspection.
53    name: String,
54    /// The parameter types, in order, as the function was compiled.
55    params: Vec<Type>,
56    /// The return type as the function was compiled.
57    ret: Type,
58    /// The length of the emitted machine code in bytes (`<=` the region length, which
59    /// is rounded up to whole pages).
60    code_len: usize,
61}
62
63impl Compiled {
64    /// Builds the handle from its already-prepared parts. Crate-internal: a `Compiled`
65    /// only ever comes from a successful [`Jit::compile`](crate::Jit::compile).
66    pub(crate) fn new(
67        region: Region,
68        name: String,
69        params: Vec<Type>,
70        ret: Type,
71        code_len: usize,
72    ) -> Self {
73        Self {
74            region,
75            name,
76            params,
77            ret,
78            code_len,
79        }
80    }
81
82    /// The compiled function's name, as it was given to the IR builder.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use jit_lang::compile;
88    /// use ir_lang::{Builder, Type};
89    ///
90    /// let mut b = Builder::new("noop", &[], Type::Unit);
91    /// b.ret(None);
92    /// assert_eq!(compile(&b.finish()).unwrap().name(), "noop");
93    /// ```
94    #[must_use]
95    pub fn name(&self) -> &str {
96        &self.name
97    }
98
99    /// The parameter types, in declaration order. These tell you the argument types of
100    /// the function-pointer you transmute to with [`entry`](Self::entry).
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// use jit_lang::compile;
106    /// use ir_lang::{Builder, Type};
107    ///
108    /// let mut b = Builder::new("f", &[Type::Int, Type::Float], Type::Int);
109    /// let x = b.block_params(b.entry())[0];
110    /// b.ret(Some(x));
111    /// assert_eq!(compile(&b.finish()).unwrap().params(), &[Type::Int, Type::Float]);
112    /// ```
113    #[must_use]
114    pub fn params(&self) -> &[Type] {
115        &self.params
116    }
117
118    /// The return type. A [`Unit`](ir_lang::Type::Unit) return means the function
119    /// yields no value, so the matching function-pointer type has no return.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use jit_lang::compile;
125    /// use ir_lang::{Builder, Type};
126    ///
127    /// let mut b = Builder::new("f", &[], Type::Unit);
128    /// b.ret(None);
129    /// assert_eq!(compile(&b.finish()).unwrap().ret(), Type::Unit);
130    /// ```
131    #[must_use]
132    pub fn ret(&self) -> Type {
133        self.ret
134    }
135
136    /// The length of the emitted machine code, in bytes.
137    ///
138    /// This is the size of the function body, which is at most the length of the
139    /// region holding it — the region is rounded up to whole pages, so it is usually
140    /// larger. Useful for inspecting or logging how much code a function compiled to.
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use jit_lang::compile;
146    /// use ir_lang::{Builder, Type};
147    ///
148    /// let mut b = Builder::new("f", &[Type::Int], Type::Int);
149    /// let x = b.block_params(b.entry())[0];
150    /// b.ret(Some(x));
151    /// // An identity function still compiles to at least one instruction.
152    /// assert!(compile(&b.finish()).unwrap().code_len() > 0);
153    /// ```
154    #[must_use]
155    pub fn code_len(&self) -> usize {
156        self.code_len
157    }
158
159    /// A raw pointer to the first byte of the compiled code — the function's entry
160    /// point.
161    ///
162    /// The pointer is valid to obtain and to compare; it is into read-execute memory,
163    /// so reading or running through it is `unsafe`. For the common case of calling
164    /// the function, prefer [`entry`](Self::entry), which produces a typed
165    /// function-pointer. The pointer is valid only while this `Compiled` is alive.
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// use jit_lang::compile;
171    /// use ir_lang::{Builder, Type};
172    ///
173    /// let mut b = Builder::new("f", &[], Type::Unit);
174    /// b.ret(None);
175    /// let f = compile(&b.finish()).unwrap();
176    /// assert!(!f.as_ptr().is_null());
177    /// ```
178    #[must_use]
179    pub fn as_ptr(&self) -> *const u8 {
180        self.region.as_ptr()
181    }
182
183    /// Reinterprets the entry point as a function pointer of type `F` so the compiled
184    /// code can be called.
185    ///
186    /// This is how you run a compiled function: pick an `extern "C"` function-pointer
187    /// type whose signature matches what was compiled, get it from `entry`, and call
188    /// it. The mapping from IR types to the C ABI is `int` to a 64-bit integer,
189    /// `float` to `f64`, `bool` to a byte that is `0` or `1`, and a `unit` return to no
190    /// return value. The compiled code uses the host C calling convention, which is
191    /// what `extern "C"` denotes, so the two line up.
192    ///
193    /// # Safety
194    ///
195    /// The caller guarantees all of the following:
196    ///
197    /// - `F` is a function-pointer type (so it is pointer-sized, the size of the entry
198    ///   address). Passing a non-function or differently-sized type is undefined
199    ///   behavior.
200    /// - `F`'s signature matches the compiled function: one `extern "C"` argument per
201    ///   entry in [`params`](Self::params) with the ABI type above, and a return that
202    ///   matches [`ret`](Self::ret). A mismatch is undefined behavior.
203    /// - Every call through the returned pointer happens while `self` is alive. Once
204    ///   `self` is dropped the code is unmapped and the pointer dangles.
205    ///
206    /// # Examples
207    ///
208    /// Call a compiled `fn double(x: int) -> int`:
209    ///
210    /// ```
211    /// use jit_lang::compile;
212    /// use ir_lang::{Builder, BinOp, Type};
213    ///
214    /// let mut b = Builder::new("double", &[Type::Int], Type::Int);
215    /// let x = b.block_params(b.entry())[0];
216    /// let sum = b.bin(BinOp::Add, x, x);
217    /// b.ret(Some(sum));
218    /// let f = compile(&b.finish()).unwrap();
219    ///
220    /// // SAFETY: the signature matches `fn double(x: int) -> int`, and `f` outlives the call.
221    /// let double: extern "C" fn(i64) -> i64 = unsafe { f.entry() };
222    /// assert_eq!(double(21), 42);
223    /// assert_eq!(double(-5), -10);
224    /// ```
225    ///
226    /// Call one that returns a `bool`:
227    ///
228    /// ```
229    /// use jit_lang::compile;
230    /// use ir_lang::{Builder, BinOp, Type};
231    ///
232    /// // fn lt(a: int, b: int) -> bool { a < b }
233    /// let mut b = Builder::new("lt", &[Type::Int, Type::Int], Type::Bool);
234    /// let a = b.block_params(b.entry())[0];
235    /// let c = b.block_params(b.entry())[1];
236    /// let lt = b.bin(BinOp::Lt, a, c);
237    /// b.ret(Some(lt));
238    /// let f = compile(&b.finish()).unwrap();
239    ///
240    /// // SAFETY: a `bool` return is a 0/1 byte, which `extern "C" fn(..) -> bool` reads correctly.
241    /// let lt: extern "C" fn(i64, i64) -> bool = unsafe { f.entry() };
242    /// assert!(lt(1, 2));
243    /// assert!(!lt(2, 1));
244    /// ```
245    #[must_use]
246    pub unsafe fn entry<F: Copy>(&self) -> F {
247        debug_assert_eq!(
248            mem::size_of::<F>(),
249            mem::size_of::<*const u8>(),
250            "entry::<F>() requires F to be a pointer-sized function pointer",
251        );
252        let ptr = self.as_ptr();
253        // SAFETY: `ptr` is the entry address of the compiled function. The caller's
254        // `# Safety` contract guarantees `F` is a function-pointer type — hence
255        // pointer-sized — whose signature matches the compiled code, so reinterpreting
256        // the address as `F` produces a valid, callable pointer. `transmute_copy` reads
257        // `size_of::<F>()` bytes from `&ptr`; with `F` pointer-sized that is exactly the
258        // pointer, checked in debug by the assertion above.
259        unsafe { mem::transmute_copy::<*const u8, F>(&ptr) }
260    }
261}
262
263impl std::fmt::Debug for Compiled {
264    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265        f.debug_struct("Compiled")
266            .field("name", &self.name)
267            .field("params", &self.params)
268            .field("ret", &self.ret)
269            .field("code_len", &self.code_len)
270            .field("entry", &self.as_ptr())
271            .finish()
272    }
273}