Documentation
The Top Level
The top-level of a Helix declaration is a call to the ruby! {}
macro.
[dependencies]
helix = "*"
#[macro_use]
extern crate helix;
ruby! {
}
Classes
Inside the ruby!
macro, you create a new class using the class
keyword.
Classes open with a {
and close with a }
.
#[macro_use]
extern crate helix;
ruby! {
class Console {
}
}
Methods
Inside a class, start methods with a def
followed by the name of the method,
followed by a normal Rust method signature.
#[macro_use]
extern crate helix;
ruby! {
class Console {
def log(message: String) {
println!("LOG: {:?}", message);
}
}
}
>> require 'console'
>> Console.log("Hi from Rust!");
"LOG: Hi from Rust"
Class Methods vs. Instance Methods
Instance methods take &self
or &mut self
as their first parameter, like
Rust instance methods. Class methods don’t have any kind of self
as their
first parameter.
Bare
self
, without&
or&mut
, is not currently supported by Helix.
#[macro_use]
extern crate helix;
ruby! {
class Console {
// class method
def log(message: String) {
println!("LOG: {:?}", message);
}
// instance method
def warn(&self, message: String) {
println!("WARN: {:?}", message);
}
}
}
>> require 'console'
>> Console.log("Hi from Rust!");
"LOG: Hi from Rust"
>> Console.new.warn("Hi from Rust. Stern warning")
"WARN: Hi from Rust. Stern warning"
Parameters
Methods defined in a Helix class can take any number of parameters formatted the same way as parameters in normal Rust methods.
Parameters may not currently have the
mut
modifier. The part of a parameter before the colon must be a simple identifier.
Valid Parameter Types
Helix methods take Rust types as parameters, and defines a protocol that automatically coerces Ruby values into the Rust types you specify.
Helix automatically supplies coercions from the Ruby values on the left of the following table to the Rust values on the right.
Ruby | Rust | Rules |
---|---|---|
String |
String |
Ruby string must be valid UTF-8 |
true or false |
bool |
|
Float or Integer |
f64 |
Ruby Integers must not overflow max safe precision of f64 |
Integer |
u64 , i64 , u32 , i32 |
Ruby Integers must not overflow or underflow the Rust type |
Any supported value or nil |
Option<T> |
|
Instance of a Helix class | &HelixClass or &mut HelixClass |
|
nil | () |
You can use any of the types in the Rust
column of the above table in
your methods. Helix will automatically type check any Ruby values passed
to the method, and raise a TypeError
if any of the Ruby values don’t
match the rules for that type.
#[macro_use]
extern crate helix;
ruby! {
class Console {
// class method
def log(message: String, should_print: bool) {
if should_print {
println!("LOG: {:?}", message);
}
}
}
}
>> require 'console'
>> Console.log("Hi from Rust!")
ArgumentError: wrong number of arguments (given 1, expected 2)
>> Console.log(true, true)
TypeError: no implicit conversion from TrueClass into Rust bool
>> Console.log("Hi from Rust", true)
"LOG: Hi from Rust"
>> Console.log("Hi from Rust", false)
>>
Valid Return Types
Similarly, Helix methods use Rust types in their signatures as return types. Helix also defines a protocol for converting Rust types back into Ruby.
Rust | Ruby | Rules |
---|---|---|
String |
String |
Ruby string must be valid UTF-8 |
bool |
true or false |
|
f64 |
Float |
|
u64 , i64 , u32 , i32 |
Integer |
|
Option<T> |
T or nil |
If Some(T) , coerce the T as normal; if None , return nil |
HelixClass |
Ruby instance of HelixClass |
|
() |
nil |
#[macro_use]
extern crate helix;
ruby! {
class Calculator {
def multiply(left: u64, right: u64) -> u64 {
left * right
}
}
}
>> require 'calculator'
>> Calculator.multiply(100, 10)
1000
Thinking in Helix
Most of the time, the easiest way to think about Helix classes is that you define methods with Rust arguments and Rust return values, and Ruby code can call those methods with the Ruby equivalents of those values.
For example, if a method takes a u64
, it should be documented as a Ruby method
taking an Integer
that must be smaller than 2 ** 64
. If a method takes a String
,
it should be documented as a Ruby method taking a String
that must be valid UTF-8.
Similarly, if a method return Option<String>
, it should be documented as a
Ruby method that returns either a UTF-8 String
or nil
.
Instance Fields
A Helix class can define Rust fields that its instance methods will have access to.
These serve the same role as instance variables in Ruby, but can be heavily optimized by the Rust compiler.
Defining Fields
A Helix class defines ints fields in a struct
block, which must be the first
item inside a class.
The following example won’t work until the next section, when we define the initializer for this class.
#[macro_use]
extern crate helix;
ruby! {
class Line {
struct {
x1: f64,
y1: f64,
x2: f64,
y2: f64
}
def distance(&self) -> f64 {
let (dx, dy) = (self.x1 - self.x2, self.y1 - self.y2);
dx.hypot(dy)
}
}
class Point {
struct {
x: f64,
y: f64
}
def join(&self, second: &Point) -> Line {
Line {
x1: self.x,
y1: self.y,
x2: second.x,
y2: second.y
}
}
}
}
Initialization
A Helix class with a struct
must also define an initializer.
#[macro_use]
extern crate helix;
ruby! {
class Line {
struct {
x1: f64,
y1: f64,
x2: f64,
y2: f64
}
def initialize(helix, p1: &Point, p2: &Point) {
Line { helix, x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }
}
def distance(&self) -> f64 {
let (dx, dy) = (self.x1 - self.x2, self.y1 - self.y2);
dx.hypot(dy)
}
}
class Point {
struct {
x: f64,
y: f64
}
def initialize(helix, x: f64, y: f64) {
Point { helix, x, y }
}
def join(&self, second: &Point) -> Line {
Line::new(self, second)
}
}
}
(See the LinePoint Demo for the full example.)
The initialize
method has a few differences from normal methods:
- The first parameter must be named
helix
and has no specified type - The method has no explicit return type, but must return an instance of the current class
- The returned struct must have all of the fields specified in
struct
, as well as the specialhelix
field. You must supply thehelix
parameter in that position.
The
helix
parameter is an opaque type that wraps internal bookkeeping information that Helix needs to seamlessly make your Rust type available to your methods.
The Coercion Protocol
While Helix supplies a number of coercions for you out of the box (with more coercions for basic Ruby and Rust types coming all the time), it may sometimes be important to define your own coercions for your own types.
For example, if you’ve written a Rust date-time library, you might want to
define the coercion from Ruby’s DateTime
, Time
and Date
to your Rust
DateTime
.