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}