Skip to content

Commit

Permalink
Document rwf-ruby
Browse files Browse the repository at this point in the history
  • Loading branch information
levkk committed Nov 30, 2024
1 parent 0d325a8 commit 4ee7be1
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 17 deletions.
6 changes: 6 additions & 0 deletions rwf-ruby/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Rwf ruby

`rwf-ruby` contains Rust bindings for running Ruby applications built on top of [Rack](https://github.com/rack/rack). While there exists other projects that bind Ruby to Rust in a generic way,
running arbitrary Ruby code inside Rust requires wrapping the `ruby_exec_node` directly.

This project is experimental, and needs additional testing to ensure production stability. The bindings are written in C, see [src/bindings.c](src/bindings.c).
3 changes: 1 addition & 2 deletions rwf-ruby/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::process::Command;
fn main() {
println!("cargo:rerun-if-changed=src/bindings.c");
println!("cargo:rerun-if-changed=src/bindings.h"); // Bindings are generated manually because bindgen goes overboard with ruby.h
// println!("cargo:rustc-link-arg=-l/opt/homebrew/Cellar/ruby/3.3.4/lib/libruby.dylib");

let output = Command::new("ruby")
.arg("headers.rb")
Expand All @@ -18,7 +17,7 @@ fn main() {
build.flag(flag);
}

// Github actions workaround
// Github actions workaround. I don't remember if this works or not.
match Command::new("find")
.arg("/opt/hostedtoolcache/Ruby")
.arg("-name")
Expand Down
2 changes: 0 additions & 2 deletions rwf-ruby/src/Makefile

This file was deleted.

8 changes: 8 additions & 0 deletions rwf-ruby/src/bindings.c
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ RackResponse rwf_rack_response_new(VALUE value) {
return response;
}

/*
* Convert bytes to a StringIO wrapped into a Rack InputWrapper expected by Rails.
*/
static VALUE rwf_request_body(const char *body) {
VALUE rb_str = rb_str_new_cstr(body);
VALUE str_io = rwf_get_class("StringIO");
Expand Down Expand Up @@ -263,6 +266,11 @@ void rwf_clear_error_state() {
rb_set_errinfo(Qnil);
}

/*
* Print and clear an exception.
* Used for debugging. We don't really expect Rack to throw an exception; that would
* mean there is a bug in Rails exception handling.
*/
int rwf_print_error() {
VALUE error = rb_errinfo();

Expand Down
53 changes: 52 additions & 1 deletion rwf-ruby/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! Rust wrapper over the C bindings to Ruby.
use libc::uintptr_t;
use once_cell::sync::OnceCell;
use std::ffi::{c_char, c_int, CStr, CString};
Expand All @@ -11,35 +12,60 @@ use tracing::info;
// Make sure the Ruby VM is initialized only once.
static RUBY_INIT: OnceCell<Ruby> = OnceCell::new();

/// Response generated by a Rack application.
///
/// The `VALUE` returned by Ruby is kept to ensure
/// the garbage collector doesn't run while we're processing this response.
#[repr(C)]
#[derive(Debug, Clone)]
pub struct RackResponse {
/// Ruby object reference.
pub value: uintptr_t,

/// Response code, e.g. `200`.
pub code: c_int,

/// Number of HTTP headers in the response.
pub num_headers: c_int,

/// Header key/value pairs.
pub headers: *mut KeyValue,

/// Response body as bytes.
pub body: *mut c_char,

/// 1 if this is a file, 0 if its bytes.
pub is_file: c_int,
}

/// Header key/value pair.
///
/// Memory is allocated and de-allocated in C. Rust is just borrowing it.
#[repr(C)]
#[derive(Debug)]
pub struct KeyValue {
key: *const c_char,
value: *const c_char,
}

/// Rack request, converted from an Rwf request.
#[repr(C)]
#[derive(Debug)]
pub struct RackRequest {
// ENV object.
env: *const KeyValue,
// Number of entries in ENV.
length: c_int,
// Request body as bytes.
body: *const c_char,
}

impl RackRequest {
/// Send a request to Rack and get a response.
///
/// `env` must follow the Rack spec and contain HTTP headers, and other request metadata.
/// `body` contains the request body as bytes.
pub fn send(env: HashMap<String, String>, body: &[u8]) -> Result<RackResponse, Error> {
// let mut c_strings = vec![];
let mut keys = vec![];

let (mut k, mut v) = (vec![], vec![]);
Expand Down Expand Up @@ -82,6 +108,9 @@ impl RackRequest {
}

/// RackResponse with values allocated in Rust memory space.
///
/// Upon receiving a response from Rack, we copy data into Rust
/// and release C-allocated memory so the Ruby garbage collector can run.
#[derive(Debug)]
pub struct RackResponseOwned {
code: u16,
Expand Down Expand Up @@ -208,6 +237,7 @@ extern "C" {
) -> c_int;
}

/// Errors returned from Ruby.
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Ruby VM did not start")]
Expand All @@ -220,11 +250,13 @@ pub enum Error {
App,
}

/// Wrapper around Ruby's `VALUE`.
#[derive(Debug)]
pub struct Value {
ptr: uintptr_t,
}

/// Ruby object data types.
#[derive(Debug, PartialEq)]
#[repr(C)]
pub enum Type {
Expand Down Expand Up @@ -262,6 +294,8 @@ pub enum Type {
}

impl Value {
/// Convert `VALUE` to a Rust String. If `VALUE` is not a string,
/// an empty string is returned.
pub fn to_string(&self) -> String {
if self.ty() == Type::RString {
unsafe {
Expand All @@ -273,6 +307,8 @@ impl Value {
}
}

/// Get `VALUE` data type.
/// TODO: this function isn't fully implemented.
pub fn ty(&self) -> Type {
let ty = unsafe { rwf_rb_type(self.ptr) };
match ty {
Expand All @@ -281,6 +317,7 @@ impl Value {
}
}

/// Get the raw `VALUE` pointer.
pub fn raw_ptr(&self) -> uintptr_t {
self.ptr
}
Expand All @@ -292,9 +329,13 @@ impl From<uintptr_t> for Value {
}
}

/// Wrapper around the Ruby VM.
pub struct Ruby;

impl Ruby {
/// Initialize the Ruby VM.
///
/// Safe to call multiple times. The VM is initialized only once.
pub fn init() -> Result<(), Error> {
RUBY_INIT.get_or_try_init(move || Ruby::new())?;

Expand Down Expand Up @@ -353,12 +394,14 @@ impl Ruby {
}
}

/// Disable the garbage collector.
pub fn gc_disable() {
unsafe {
rb_gc_disable();
}
}

/// Enable the garbage collector.
pub fn gc_enable() {
unsafe {
rb_gc_enable();
Expand All @@ -377,6 +420,7 @@ impl Drop for Ruby {
#[cfg(test)]
mod test {
use super::*;
use std::env::var;

#[test]
fn test_rack_response() {
Expand All @@ -399,6 +443,13 @@ mod test {

#[test]
fn test_load_rails() {
#[cfg(target_os = "linux")]
if var("GEM_HOME").is_err() {
panic!(
"GEM_HOME isn't set. This test will most likely fail to load Ruby deps and crash."
);
}

Ruby::load_app(&Path::new("tests/todo/config/environment.rb")).unwrap();
let response = Ruby::eval("Rails.application.call({})").unwrap();
let response = RackResponse::new(&response);
Expand Down
12 changes: 0 additions & 12 deletions rwf-ruby/src/main.c

This file was deleted.

0 comments on commit 4ee7be1

Please sign in to comment.