rwf_ruby/
lib.rs

1//! Rust wrapper over the C bindings to Ruby.
2use libc::uintptr_t;
3use once_cell::sync::OnceCell;
4use std::ffi::{c_char, c_int, CStr, CString};
5use std::fs::canonicalize;
6use std::mem::MaybeUninit;
7use std::path::Path;
8
9use std::collections::HashMap;
10use tracing::info;
11
12// Make sure the Ruby VM is initialized only once.
13static RUBY_INIT: OnceCell<Ruby> = OnceCell::new();
14
15/// Response generated by a Rack application.
16///
17/// The `VALUE` returned by Ruby is kept to ensure
18/// the garbage collector doesn't run while we're processing this response.
19#[repr(C)]
20#[derive(Debug, Clone)]
21pub struct RackResponse {
22    /// Ruby object reference.
23    pub value: uintptr_t,
24
25    /// Response code, e.g. `200`.
26    pub code: c_int,
27
28    /// Number of HTTP headers in the response.
29    pub num_headers: c_int,
30
31    /// Header key/value pairs.
32    pub headers: *mut KeyValue,
33
34    /// Response body as bytes.
35    pub body: *mut c_char,
36
37    /// 1 if this is a file, 0 if its bytes.
38    pub is_file: c_int,
39}
40
41/// Header key/value pair.
42///
43/// Memory is allocated and de-allocated in C. Rust is just borrowing it.
44#[repr(C)]
45#[derive(Debug)]
46pub struct KeyValue {
47    key: *const c_char,
48    value: *const c_char,
49}
50
51/// Rack request, converted from an Rwf request.
52#[repr(C)]
53#[derive(Debug)]
54pub struct RackRequest {
55    // ENV object.
56    env: *const KeyValue,
57    // Number of entries in ENV.
58    length: c_int,
59    // Request body as bytes.
60    body: *const c_char,
61}
62
63impl RackRequest {
64    /// Send a request to Rack and get a response.
65    ///
66    /// `env` must follow the Rack spec and contain HTTP headers, and other request metadata.
67    /// `body` contains the request body as bytes.
68    pub fn send(env: HashMap<String, String>, body: &[u8]) -> Result<RackResponse, Error> {
69        let mut keys = vec![];
70
71        let (mut k, mut v) = (vec![], vec![]);
72
73        for (key, value) in &env {
74            let key = CString::new(key.as_str()).unwrap();
75            let value = CString::new(value.as_str()).unwrap();
76            k.push(key);
77            v.push(value);
78
79            let env_key = KeyValue {
80                key: k.last().unwrap().as_ptr(),
81                value: v.last().unwrap().as_ptr(),
82            };
83
84            keys.push(env_key);
85        }
86
87        let body = CString::new(body).unwrap();
88
89        let req = RackRequest {
90            length: keys.len() as c_int,
91            env: keys.as_ptr(),
92            body: body.as_ptr(),
93        };
94
95        // Hardcoded to Rails, but can be any other Rack app.
96        let app_name = CString::new("Rails.application").unwrap();
97
98        let mut response: RackResponse = unsafe { MaybeUninit::zeroed().assume_init() };
99
100        let result = unsafe { rwf_app_call(req, app_name.as_ptr(), &mut response) };
101
102        if result != 0 {
103            return Err(Error::App);
104        } else {
105            Ok(response)
106        }
107    }
108}
109
110/// RackResponse with values allocated in Rust memory space.
111///
112/// Upon receiving a response from Rack, we copy data into Rust
113/// and release C-allocated memory so the Ruby garbage collector can run.
114#[derive(Debug)]
115pub struct RackResponseOwned {
116    code: u16,
117    headers: HashMap<String, String>,
118    body: Vec<u8>,
119    is_file: bool,
120}
121
122impl RackResponseOwned {
123    /// Request body.
124    pub fn body(&self) -> &[u8] {
125        &self.body
126    }
127
128    /// Request HTTP code.
129    pub fn code(&self) -> u16 {
130        self.code
131    }
132
133    /// Is the request a file?
134    pub fn is_file(&self) -> bool {
135        self.is_file
136    }
137
138    /// Request headers.
139    pub fn headers(&self) -> &HashMap<String, String> {
140        &self.headers
141    }
142}
143
144impl From<RackResponse> for RackResponseOwned {
145    /// Move all data out of C into Rust-owned memory.
146    /// This also drops the reference to the Rack response array,
147    /// allowing it to be garbage collected.
148    fn from(response: RackResponse) -> RackResponseOwned {
149        let code = response.code as u16;
150
151        let mut headers = HashMap::new();
152
153        for n in 0..response.num_headers {
154            let env_key = unsafe { response.headers.offset(n as isize) };
155            let name = unsafe { CStr::from_ptr((*env_key).key) };
156            let value = unsafe { CStr::from_ptr((*env_key).value) };
157
158            // Headers should be valid UTF-8.
159            headers.insert(
160                name.to_string_lossy().to_string(),
161                value.to_string_lossy().to_string(),
162            );
163        }
164
165        // Body can be anything.
166        let body = unsafe { CStr::from_ptr(response.body) };
167        let body = Vec::from(body.to_bytes());
168
169        RackResponseOwned {
170            code,
171            headers,
172            body,
173            is_file: response.is_file == 1,
174        }
175    }
176}
177
178impl RackResponse {
179    /// Parse the Rack response from a Ruby value.
180    pub fn new(value: &Value) -> Self {
181        unsafe { rwf_rack_response_new(value.raw_ptr()) }
182    }
183}
184
185impl Drop for RackResponse {
186    fn drop(&mut self) {
187        unsafe { rwf_rack_response_drop(self) }
188    }
189}
190
191#[link(name = "ruby")]
192extern "C" {
193    fn ruby_cleanup(code: c_int) -> c_int;
194    fn rb_errinfo() -> uintptr_t;
195
196    // Execute some Ruby code.
197    fn rb_eval_string_protect(code: *const c_char, state: *mut c_int) -> uintptr_t;
198    fn rb_obj_as_string(value: uintptr_t) -> uintptr_t;
199
200    fn rb_gc_disable() -> c_int;
201    fn rb_gc_enable() -> c_int;
202}
203
204#[link(name = "rwf_ruby")]
205extern "C" {
206    /// Get the type of the object.
207    fn rwf_rb_type(value: uintptr_t) -> c_int;
208
209    /// Get the CStr value. Careful with this one,
210    /// if the object isn't a string, this will segfault.
211    fn rwf_value_cstr(value: uintptr_t) -> *mut c_char;
212
213    /// Clear error state after handling an exception.
214    fn rwf_clear_error_state();
215
216    /// Convert the Rack response to a struct we can work with.
217    /// The Rack response is an array of three elements:
218    /// - HTTP code (int)
219    /// - headers (Hash)
220    /// - body (String)
221    fn rwf_rack_response_new(value: uintptr_t) -> RackResponse;
222
223    /// Deallocate memory allocated for converting the Rack response
224    /// from Ruby to Rust.
225    fn rwf_rack_response_drop(response: &RackResponse);
226
227    /// Load an app into the VM.
228    fn rwf_load_app(path: *const c_char) -> c_int;
229
230    /// Initialize Ruby correctly.
231    fn rwf_init_ruby();
232
233    fn rwf_app_call(
234        request: RackRequest,
235        app_name: *const c_char,
236        response: *mut RackResponse,
237    ) -> c_int;
238}
239
240/// Errors returned from Ruby.
241#[derive(Debug, thiserror::Error)]
242pub enum Error {
243    #[error("Ruby VM did not start")]
244    VmInit,
245
246    #[error("{err}")]
247    Eval { err: String },
248
249    #[error("Ruby app failed to load")]
250    App,
251}
252
253/// Wrapper around Ruby's `VALUE`.
254#[derive(Debug)]
255pub struct Value {
256    ptr: uintptr_t,
257}
258
259/// Ruby object data types.
260#[derive(Debug, PartialEq)]
261#[repr(C)]
262pub enum Type {
263    None = 0x00,
264
265    Object = 0x01,
266    Class = 0x02,
267    Module = 0x03,
268    Float = 0x04,
269    RString = 0x05,
270    Regexp = 0x06,
271    Array = 0x07,
272    Hash = 0x08,
273    Struct = 0x09,
274    Bignum = 0x0a,
275    File = 0x0b,
276    Data = 0x0c,
277    Match = 0x0d,
278    Complex = 0x0e,
279    Rational = 0x0f,
280
281    Nil = 0x11,
282    True = 0x12,
283    False = 0x13,
284    Symbol = 0x14,
285    Fixnum = 0x15,
286    Undef = 0x16,
287
288    IMemo = 0x1a,
289    Node = 0x1b,
290    IClass = 0x1c,
291    Zombie = 0x1d,
292
293    Mask = 0x1f,
294}
295
296impl Value {
297    /// Convert `VALUE` to a Rust String. If `VALUE` is not a string,
298    /// an empty string is returned.
299    pub fn to_string(&self) -> String {
300        if self.ty() == Type::RString {
301            unsafe {
302                let cstr = rwf_value_cstr(self.ptr);
303                CStr::from_ptr(cstr).to_string_lossy().to_string()
304            }
305        } else {
306            String::new()
307        }
308    }
309
310    /// Get `VALUE` data type.
311    /// TODO: this function isn't fully implemented.
312    pub fn ty(&self) -> Type {
313        let ty = unsafe { rwf_rb_type(self.ptr) };
314        match ty {
315            0x05 => Type::RString,
316            _ => Type::Nil,
317        }
318    }
319
320    /// Get the raw `VALUE` pointer.
321    pub fn raw_ptr(&self) -> uintptr_t {
322        self.ptr
323    }
324}
325
326impl From<uintptr_t> for Value {
327    fn from(ptr: uintptr_t) -> Value {
328        Value { ptr }
329    }
330}
331
332/// Wrapper around the Ruby VM.
333pub struct Ruby;
334
335impl Ruby {
336    /// Initialize the Ruby VM.
337    ///
338    /// Safe to call multiple times. The VM is initialized only once.
339    pub fn init() -> Result<(), Error> {
340        RUBY_INIT.get_or_try_init(move || Ruby::new())?;
341
342        Ok(())
343    }
344
345    fn new() -> Result<Self, Error> {
346        unsafe {
347            rwf_init_ruby();
348            Ok(Ruby {})
349        }
350    }
351
352    /// Preload the Rack app into memory. Run this before trying to run anything else.
353    pub fn load_app(path: impl AsRef<Path> + Copy) -> Result<(), Error> {
354        Self::init()?;
355        let path = path.as_ref();
356
357        let version = Self::eval("RUBY_VERSION").unwrap().to_string();
358        info!("Using {}", version);
359
360        if path.exists() {
361            // We use `require`, which only works with abslute paths.
362            let absolute = canonicalize(path).unwrap();
363            let s = absolute.display().to_string();
364            let cs = CString::new(s).unwrap();
365
366            unsafe {
367                if rwf_load_app(cs.as_ptr()) != 0 {
368                    return Err(Error::App);
369                }
370            }
371        }
372
373        Ok(())
374    }
375
376    /// Run some Ruby code. If an exception is thrown, return the error.
377    pub fn eval(code: &str) -> Result<Value, Error> {
378        Self::init()?;
379
380        unsafe {
381            let mut state: c_int = 0;
382            let c_string = CString::new(code).unwrap();
383            let value = rb_eval_string_protect(c_string.as_ptr(), &mut state);
384
385            if state != 0 {
386                let err = rb_errinfo();
387                let err = Value::from(rb_obj_as_string(err)).to_string();
388                rwf_clear_error_state();
389
390                Err(Error::Eval { err })
391            } else {
392                Ok(Value { ptr: value })
393            }
394        }
395    }
396
397    /// Disable the garbage collector.
398    pub fn gc_disable() {
399        unsafe {
400            rb_gc_disable();
401        }
402    }
403
404    /// Enable the garbage collector.
405    pub fn gc_enable() {
406        unsafe {
407            rb_gc_enable();
408        }
409    }
410}
411
412impl Drop for Ruby {
413    fn drop(&mut self) {
414        unsafe {
415            ruby_cleanup(0);
416        }
417    }
418}
419
420#[cfg(test)]
421mod test {
422    use super::*;
423    use std::env::var;
424
425    #[test]
426    fn test_rack_response() {
427        let response = Ruby::eval(r#"[200, {"hello": "world", "the year is 2024": "linux desktop is coming"}, ["apples and oranges"]]"#).unwrap();
428        let response = RackResponse::new(&response);
429
430        assert_eq!(response.code, 200);
431        assert_eq!(response.num_headers, 2);
432
433        let owned = RackResponseOwned::from(response);
434        assert_eq!(
435            owned.headers.get("the year is 2024"),
436            Some(&String::from("linux desktop is coming"))
437        );
438        assert_eq!(
439            String::from_utf8_lossy(&owned.body),
440            "apples and oranges".to_string()
441        );
442    }
443
444    #[test]
445    fn test_load_rails() {
446        #[cfg(target_os = "linux")]
447        if var("GEM_HOME").is_err() {
448            panic!(
449                "GEM_HOME isn't set. This test will most likely fail to load Ruby deps and crash."
450            );
451        }
452
453        Ruby::load_app(&Path::new("tests/todo/config/environment.rb")).unwrap();
454        let response = Ruby::eval("Rails.application.call({})").unwrap();
455        let response = RackResponse::new(&response);
456        let owned = RackResponseOwned::from(response);
457        assert_eq!(owned.code, 403);
458    }
459}