1use 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
12static RUBY_INIT: OnceCell<Ruby> = OnceCell::new();
14
15#[repr(C)]
20#[derive(Debug, Clone)]
21pub struct RackResponse {
22 pub value: uintptr_t,
24
25 pub code: c_int,
27
28 pub num_headers: c_int,
30
31 pub headers: *mut KeyValue,
33
34 pub body: *mut c_char,
36
37 pub is_file: c_int,
39}
40
41#[repr(C)]
45#[derive(Debug)]
46pub struct KeyValue {
47 key: *const c_char,
48 value: *const c_char,
49}
50
51#[repr(C)]
53#[derive(Debug)]
54pub struct RackRequest {
55 env: *const KeyValue,
57 length: c_int,
59 body: *const c_char,
61}
62
63impl RackRequest {
64 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 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#[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 pub fn body(&self) -> &[u8] {
125 &self.body
126 }
127
128 pub fn code(&self) -> u16 {
130 self.code
131 }
132
133 pub fn is_file(&self) -> bool {
135 self.is_file
136 }
137
138 pub fn headers(&self) -> &HashMap<String, String> {
140 &self.headers
141 }
142}
143
144impl From<RackResponse> for RackResponseOwned {
145 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.insert(
160 name.to_string_lossy().to_string(),
161 value.to_string_lossy().to_string(),
162 );
163 }
164
165 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 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 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 fn rwf_rb_type(value: uintptr_t) -> c_int;
208
209 fn rwf_value_cstr(value: uintptr_t) -> *mut c_char;
212
213 fn rwf_clear_error_state();
215
216 fn rwf_rack_response_new(value: uintptr_t) -> RackResponse;
222
223 fn rwf_rack_response_drop(response: &RackResponse);
226
227 fn rwf_load_app(path: *const c_char) -> c_int;
229
230 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#[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#[derive(Debug)]
255pub struct Value {
256 ptr: uintptr_t,
257}
258
259#[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 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 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 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
332pub struct Ruby;
334
335impl Ruby {
336 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 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 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 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 pub fn gc_disable() {
399 unsafe {
400 rb_gc_disable();
401 }
402 }
403
404 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}