Skip to content

Commit

Permalink
Merge pull request #125 from erickguan/docs
Browse files Browse the repository at this point in the history
Update wrap and TypedData documentation
  • Loading branch information
matsadler authored Dec 20, 2024
2 parents 006693f + 6b07ffb commit 4f44655
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 99 deletions.
64 changes: 54 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,24 @@ make sure the return type is `magnus::Value`.

### Wrapping Rust Types in Ruby Objects

Rust structs and enums can be wrapped in Ruby objects so they can be returned
to Ruby.
Magnus allows you to wrap Rust structs and enums as Ruby objects, enabling seamless interaction between Rust and Ruby. This functionality is ideal for exposing Rust logic to Ruby modules.

Types can opt-in to this with the `magnus::wrap` macro (or by implementing
`magnus::TypedData`). Whenever a compatible type is returned to Ruby it will be
wrapped in the specified class, and whenever it is passed back to Rust it will
be unwrapped to a reference.
Use one of the following approaches to expose a Rust type to Ruby:

* A convenience macro [`#[magnus::wrap]`].
* More customised approach by implementing the [`magnus::TypedData`] trait.

[`#[magnus::wrap]`]: https://docs.rs/magnus/latest/magnus/attr.wrap.html
[`magnus::TypedData`]: https://docs.rs/magnus/latest/magnus/derive.TypedData.html

Then this Rust type can be:

* Returned to Ruby as a wrapped object.
* Passed back to Rust and automatically unwrapped to a native Rust reference.

#### Basic Usage

Here’s how you can wrap a simple Rust struct and expose its methods to Ruby:

```rust
use magnus::{function, method, prelude::*, Error, Ruby};
Expand Down Expand Up @@ -128,16 +139,20 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
}
```

The newtype pattern and `RefCell` can be used if mutability is required:
#### Handling Mutability

Because Ruby's GC manages the memory where your Rust type is stored, Magnus can't bind functions with mutable references. To allow mutable fields in wrapped Rust structs, you can use the newtype pattern with `RefCell`:

```rust
use std::cell::RefCell;

struct Point {
x: isize,
y: isize,
}

#[magnus::wrap(class = "Point")]
struct MutPoint(std::cell::RefCell<Point>);
struct MutPoint(RefCell<Point>);

impl MutPoint {
fn set_x(&self, i: isize) {
Expand All @@ -146,8 +161,17 @@ impl MutPoint {
}
```

To allow wrapped types to be subclassed they must implement `Default`, and
define and alloc func and an initialize method:
See [`examples/mut_point.rs`] for the complete example.

[`examples/mut_point.rs`]: https://github.com/matsadler/magnus/blob/main/examples/mut_point.rs

#### Supporting Subclassing

To enable Ruby subclassing for wrapped Rust types, the type must:

* Implement the `Default` trait.
* Define an allocator.
* Define an initialiser.

``` rust
#[derive(Default)]
Expand Down Expand Up @@ -177,6 +201,26 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
}
```

#### Error Handling

Use `magnus::Error` to propagate errors to Ruby from Rust:

```rust
#[magnus::wrap(class = "Point")]
struct MutPoint(RefCell<Point>);

impl MutPoint {
fn add_x(ruby: &Ruby, rb_self: &Self, val: isize) -> Result<isize, Error> {
if let Some(sum) = rb_self.0.borrow().x.checked_add(val) {
rb_self.0.borrow_mut().x = sum;
Ok(sum)
} else {
return Err(Error::new(ruby.exception_range_error(), "result out of range"));
}
}
}
```

## Getting Started

### Writing an extension gem (calling Rust from Ruby)
Expand Down
58 changes: 57 additions & 1 deletion examples/mut_point.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,42 @@
use std::cell::RefCell;

use magnus::{function, method, prelude::*, wrap};
use magnus::{function, method, prelude::*, wrap, Error, Ruby};

struct Point {
x: isize,
y: isize,
}

// The `wrap` macro wraps a Rust type in a Ruby object, enabling seamless
// integration.
//
// Magnus uses two Ruby API functions to manage the struct:
// * `rb_data_typed_object_wrap`
// * `rb_check_typeddata`
//
// # Mutability
//
// Ruby's garbage collector (GC) manages memory for wrapped objects. This
// prevents using `&mut` references because Rust requires them to be unique,
// while Ruby GC may move objects unpredictably. To address this, you can use
// [`RefCell`](https://doc.rust-lang.org/std/cell/struct.RefCell.html) to
// enable interior mutability.
//
// # Error Handling
//
// Use [`magnus::Error`](https://docs.rs/magnus/latest/magnus/struct.Error.html)
// to propagate errors to Ruby.
// For example, you can raise a Ruby exception from Rust using Ruby's
// predefined exception classes.
//
// The syntax for methods like `add_x` differs slightly from typical Rust
// struct methods because it uses the
// [`method!` macro](https://docs.rs/magnus/latest/magnus/macro.method.html):
// * The first parameter, `ruby`, gives access to Ruby's runtime.
// * The second parameter, `rb_self`, is the Ruby object being called.
//
// See [`DataTypeFunctions`](https://docs.rs/magnus/latest/magnus/derive.DataTypeFunctions.html)
// and [`TypedData`](https://docs.rs/magnus/latest/magnus/derive.TypedData.html)
#[wrap(class = "Point")]
struct MutPoint(RefCell<Point>);

Expand All @@ -23,6 +53,18 @@ impl MutPoint {
self.0.borrow_mut().x = val;
}

fn add_x(ruby: &Ruby, rb_self: &Self, val: isize) -> Result<isize, Error> {
if let Some(sum) = rb_self.0.borrow().x.checked_add(val) {
rb_self.0.borrow_mut().x = sum;
Ok(sum)
} else {
return Err(Error::new(
ruby.exception_range_error(),
"result out of range",
));
}
}

fn y(&self) -> isize {
self.0.borrow().y
}
Expand All @@ -31,6 +73,18 @@ impl MutPoint {
self.0.borrow_mut().y = val;
}

fn add_y(ruby: &Ruby, rb_self: &Self, val: isize) -> Result<isize, Error> {
if let Some(sum) = rb_self.0.borrow().y.checked_add(val) {
rb_self.0.borrow_mut().y = sum;
Ok(sum)
} else {
return Err(Error::new(
ruby.exception_range_error(),
"result out of range",
));
}
}

fn distance(&self, other: &MutPoint) -> f64 {
(((other.x() - self.x()).pow(2) + (other.y() - self.y()).pow(2)) as f64).sqrt()
}
Expand All @@ -42,8 +96,10 @@ fn main() -> Result<(), String> {
class.define_singleton_method("new", function!(MutPoint::new, 2))?;
class.define_method("x", method!(MutPoint::x, 0))?;
class.define_method("x=", method!(MutPoint::set_x, 1))?;
class.define_method("add_x", method!(MutPoint::add_x, 1))?;
class.define_method("y", method!(MutPoint::y, 0))?;
class.define_method("y=", method!(MutPoint::set_y, 1))?;
class.define_method("add_y", method!(MutPoint::add_y, 1))?;
class.define_method("distance", method!(MutPoint::distance, 1))?;

let d: f64 = ruby.eval(
Expand Down
Loading

0 comments on commit 4f44655

Please sign in to comment.