- Notifications
You must be signed in to change notification settings - Fork62
“The Tie Between Ruby and Rust.”
License
danielpclark/rutie
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Rutie — /ro͞oˈˌtī/rOOˈˌtɪ/rüˈˌtaɪ/
Integrate Ruby with your Rust application. Or integrate Rust with your Ruby application.This project allows you to do either with relative ease.
You are highly encouraged to read the source code for this project. Every method that has beenmapped from Ruby for public use insrc/class/*
isvery well documented with example code.This is the best way to take off running with Rutie. There are also integration examples in theexamples directory which are based off of this README.
This project is a continuation of:
- Using Ruby in Rust
- Using Rust in Ruby
- Custom Ruby Objects in Rust
- Variadic Functions / Splat Operator
- Migrating from Ruru to Rutie
- Safety — The Rutie Philosophy vs The Rust Philosophy on Safety
- Troubleshooting
- It panics for some Rubies on CI server tests
- Rust signal: 11, SIGSEGV: invalid memory reference
- Error while loading shared libraries: libruby.so.#.#: cannot open shared object file: No such file or directory
- Calling methods from other methods within the
methods!
macro doesn't work - Handling exceptions raised from Ruby in Rust code
- Segfault during GC when using a Ruby method written in C
- Operating System Requirements
- Contributing
- Rutie's Future
- Additional Project History
- LICENSE
First add the dependency to yourCargo.toml
file.
[dependencies]rutie = {version ="0.8",features = ["link-ruby"] }
Then in your Rust program addVM::init()
to the beginning of its code execution pathand begin to use Rutie.
use rutie::{Object,RString,VM};fntry_it(s:&str) ->String{let a =RString::new_utf8(s);// The `send` method returns an AnyObject type.let b =unsafe{ a.send("reverse",&[])};// We must try to convert the AnyObject// type back to our usable type.match b.try_convert_to::<RString>(){Ok(ruby_string) => ruby_string.to_string(),Err(_) =>"Fail!".to_string(),}}#[test]fnit_works(){// Rust projects must start the Ruby VMVM::init();assert_eq!("selppa", try_it("apples"));}fnmain(){}
Runningcargo test
should have this test pass.
You can start a Ruby project withbundle gem rutie_ruby_example
and then onceyou change into that directory runcargo init --lib
. Remove the TODOs from the gemspecfile. Add Rutie to theCargo.toml
file and define the lib type.
[dependencies]rutie = {version ="xxx" }[lib]name ="rutie_ruby_example"crate-type = ["cdylib"]
Then edit yoursrc/lib.rs
file for your Rutie code.
use rutie::{class, methods,Class,Object,RString,VM};class!(RutieExample);methods!(RutieExample, _rtself,fn pub_reverse(input:RString) ->RString{let ruby_string = input.map_err(VM::raise_ex).unwrap();RString::new_utf8(&ruby_string.to_string().chars().rev().collect::<String>())});#[allow(non_snake_case)]#[no_mangle]pubextern"C"fnInit_rutie_ruby_example(){Class::new("RutieExample",None).define(|klass|{ klass.def_self("reverse", pub_reverse);});}
And that's it for the Rust side. When using themethods!
macro orextern
functionsmake sure the method name won't clash with any others. This is why this example is prefixed withpub_
.
Now you just need to load the library in Ruby. Add therutie
gem to your gemspec or Gemfile.
# gemspecspec.add_dependency'rutie','~> 0.0.4'# Gemfilegem'rutie','~> 0.0.4'
And then load the library in your main project filelib/rutie_ruby_example.rb
.
require_relative"rutie_ruby_example/version"require"rutie"moduleRutieRubyExampleRutie.new(:rutie_ruby_example).init"Init_rutie_ruby_example",__dir__end
That's all you need to load your Ruby things from Rust. Now to write the test intest/rutie_ruby_example_test.rb
:
require_relative"test_helper"require"rutie_ruby_example"classTestRutieRubyExample <Minitest::Testdeftest_it_reversesassert_equal"selppa",RutieExample.reverse("apples")endend
Write the following intest/test_helper.rb
:
require'minitest/autorun'
Add the following snippet to your already scaffoldedRakefile
:
require"rake/testtask"Rake::TestTask.newdo |t|t.libs <<'test't.pattern='test/**/*_test.rb't.verbose=trueend
And to properly test it you will always need to runcargo build --release
wheneveryou makeany changes to the Rust code. Run the test with:
cargo build --release; raketest
Or better yet change yourRakefile
to always run thecargo build --release
beforeevery test suite run. Feel free to change the test input to prove it fails becausethe above test works as is.
To create a Ruby object in Rust that can be returned directly to Rubyit needs just a few simple things.
Here's an example excerpt of code fromFasterPath.
use rutie::types::{Value,ValueType};use rutie::{RString,AnyObject,Object,Class,VerifiedObject};pubstructPathname{value:Value}implPathname{pubfnnew(path:&str) ->Pathname{let arguments =[RString::new_utf8(path).to_any_object()];let instance =Class::from_existing("Pathname").new_instance(Some(&arguments));Pathname{value: instance.value()}}pubfnto_any_object(&self) ->AnyObject{AnyObject::from(self.value())}}implFrom<Value>forPathname{fnfrom(value:Value) ->Self{Pathname{ value}}}implObjectforPathname{#[inline]fnvalue(&self) ->Value{self.value}}implVerifiedObjectforPathname{fnis_correct_type<T:Object>(object:&T) ->bool{Class::from_existing("Pathname").case_equals(object)}fnerror_message() ->&'staticstr{"Error converting to Pathname"}}
If the class does not yet exist in Ruby you'll need to account for creatingit before generating a new instance of it. This object is now compatible tobe returned into Ruby directly from Rust/Rutie.Note that this definition ismerely a Rust compatible representation of the Ruby object and doesn't defineany Ruby methods which can be used from Ruby.
A preferred way to integrate a dynamic amount of parameters has not yet been implemented in Rutie,but you can still manage to get it done in the following way.
use rutie::{AnyObject,Array};use rutie::types::{Argc,Value};use rutie::util::str_to_cstring;use rutie::rubysys::class;use std::mem;pubexternfnexample_method(argc:Argc,argv:*constAnyObject,_rtself:AnyObject) ->AnyObject{let args =Value::from(0);unsafe{let p_argv:*constValue = mem::transmute(argv); class::rb_scan_args( argc, p_argv,str_to_cstring("*").as_ptr(),&args)};let arguments =Array::from(args);let output =// YOUR CODE HERE. Use arguments as you see fit. output.to_any_object()}
This style of code is meant to be used outside of themethods!
macro for now.You may place this method on a class or module as you normally would from amethods!
macro definition.
use rutie::{Class,Object,VM};class!(Example);// Code from abovefnmain(){VM::init();Class::new("Example",None).define(|klass|{ klass.def("example_method", example_method);});}
The Rutie project has in its plans to remove the need for anyone to write unsafe code forvariadic support and will likely be updating themethods!
macro to support this natively.
For using Rutie versions less than 0.1 the change is simple. Replace all occurrencesof the stringruru
withrutie
in your program. And if you would like to useruby-sys
code from Rutie rather than requiringruby-sys
you can change all existingreferences toruby_sys
torutie::rubysys
.
You will have additional considerations to change likeError
being removed. For that; change instances of typeruru::result::Error
torutie::AnyException
.
Migratedparse_arguments
fromVM
toutil
.
Internal changesutil
frombinding
andrubysys
have been replaced to reduce confusion and reduce duplication.
crate::util::parse_arguments
has been marked as unsafe, this is considered a breaking change in Rust.
I'm writing this section to bring to light that, as of this writing, the safety that Rust likes to guarantee for its crates and the Rutie crate aren't currently aligned. The typical Rust safety for libraries wrapping C code is to have one unsafe crate with a-sys
extension in its name and then a crate that wraps that to make it safe.
Rutie is an official fork of the project Ruru and because of this a great deal of the decisions in design for the project remain unchanged. Rutie also brought in theruby-sys
crate and treats it as an internal private API/module; and yet shares it openly for other developers to have full control to design their own API on top of it.
One of the glaring things that Rutie has that goes against the Rust Philosophy on Safety is that any of the methods that call Ruby code, can potentially raise an exception, and don't return the typeOption<AnyObject, AnyException>
will panic when an exception is raised from Ruby… which kills the application process running. The way to avoid panics is simple: either guarantee the Ruby code you're running will never raise an exception, orHandling exceptions raised from Ruby in Rust code with "protect" methods that return the typeOption<AnyObject, AnyException>
. Anyone can implement this safety by reading and understanding how theprotect methods are written in this library and working with them.
The important thing to consider as to“why doesn't every method guarantee the safety as Rust would prescribe to?” is that exception handling in Ruby isnot a zero cost abstraction. So there is a cost in performance when you need to implement it. One can easily argue that the guarantee of safety is far more important than leaving the risk in the hands of inexperienced developers. But one could also argue that it is better to leave the choice of performance cost, and the fact that exception capturing is occasionally unnecessary, up to the developer. Seeing how the legacy of design decisions is largely inherited this project leans towards the latter argument where the choice of being absolutely safe everywhere vs some extra speed in performance is up to the developer.
I'm not opposed to this project being 100% safe, but that will be a major change and a totally different API with many decisions that need to come into play. Also since this project doesn't strictly adhere to Rust safety principles, as a crate library would be expected to be, this project will not reach the stable 1.0 release as the idea of stability and safety are hand in hand.
I do like safety guarantees and as much as possible new features and language APIs will be built toward this. You can see what the design of a safe API would look like by examining theEnumerator features that have been implemented in this way (largely wrapping method names with calls toprotect_send
).
Sometimes the Ruby binary built isn't the best for the system. Be sure to compile Rubyfor that system if this is the issue. With RVM dorvm reinstall --disable-binary
withyour choice of Ruby version.
This is an indication that you haven't started a Ruby VM in Rust yet withVM::init();
. Do this oncebefore using Ruby code from Rust.
Error while loading shared libraries: libruby.so.#.#: cannot open shared object file: No such file or directory
This happens when the Rutie build is trying to link withlibruby
,but it's not found on your library search path. Either add it toLD_LIBRARY_PATH
/DYLD_LIBRARY_PATH
if you're building a standaloneprogram that callsVM::init()
, or if you're building a library toload into a running Ruby VM then you can disable linking by eithersetting the environment variableNO_LINK_RUTIE
, or enabling thecargo featureno-link
for Rutie in yourCargo.toml
like this:
[dependencies]rutie = {version="xxx",features=["no-link"]}
The way the macro is designed doesn't use the same parameter signatures you've provided andtherefore it is recommended to implement any methods you want to re-use in Rust withfunctions outside of themethods!
macro. You can simply call that new externalmethod in themethods!
macro when defining methods for Ruby to use.
If you're using any method that doesn't return aResult<AnyObject, AnyException>
thenany exception raised from the Ruby side will interfere with that Ruby thread and causeRust to panic and stop. Ruby internally uses exceptions to effect the entire thread throughan internal thread global value. To handle places where Ruby may raise an exception during Rustcode execution you should use methods that are designed to handle that.
VM::eval
Object.protect_send
Object.protect_public_send
If you are writing lower level code and want to work more directly with the internal Rubyexception you may useVM::protect
and read the source code forObject.protect_send
tosee how it's done.
One possible issue that may cause this is when you store an item in Rust in heap memory rather than the stack.
An example case that caused this issue is the following:
Class::from_existing("Pathname").new_instance(&vec![RString::new_utf8(path).to_any_object()])
Ruby's GC traces objects from the stack. Rust's Vec, on the other hand, stores elements in the heap. So Ruby's GC may not be able to find the string you created and may release it. — @irxground
To rememdy the issue it required not using Vec but rather Rust's array type to store the argument on the stack rather than the heap.
let arguments =[RString::new_utf8(path).to_any_object()];Class::from_existing("Pathname").new_instance(&arguments)
Everything is tested against 64 bit operating systems with 64 bit Ruby & Rust builds. 32 bit isn't currently supported.
Ruby 2 is supported up through 0.8 of Rutie. For usage with Ruby 2 you need to install libssl1.1 and point to it when you install. For example:
wget https://www.openssl.org/source/openssl-1.1.1l.tar.gztar xf openssl-1.1.1l.tar.gzcd openssl-1.1.1l./config --prefix=/usr/local/openssl-1.1.1l --openssldir=/usr/local/openssl-1.1.1lmakesudo make installcd ..rvm install ruby-2.7.7 --with-openssl-dir=/usr/local/openssl-1.1.1lrvm use 2.7.7
- Rust 1.26 or later
- Ruby (64 bit) 2.5 or later
NOTE: Known issues with Ruby 3.0 compatility with the GC.GC#mark
,GC#is_marked
,GC#marked_locations
do not work with Ruby 3.
- Rust 1.26 or later
- Ruby 2.5+ built with MingW (64 bit)
- MS Visual Studio (Build Tools)
Ruby needs to be compiled with the--enable shared
option. Dynamic linking to the Ruby library provides the best performance and best support. Static build support is incomplete for now.
If using RBENV then the following is recommended:
CONFIGURE_OPTS=--enable-shared rbenv install 2.7.1
You can check if your Ruby is compiled to be dynamically linked to by running the following and getting a"yes"
response.
ruby -e "pp RbConfig::CONFIG['ENABLE_SHARED']"
If you still run intold: library not found for -lruby-static
issue, try runningcargo clean
. This'll clean any artifacts from previous attempts.
If you'd like to make a pull request for adding static build support there are currently 3 methods not working with it and linking to the proper name of the ruby static lib file & path needs to be updated.
Contributors are welcome!
The code is organized in 3 main layers. Therubysys
folder is the raw mapping to Ruby C code andall the methods from there are unsafe. Thebinding
folder is where we wrap those methods to abstractaway all the unsafe methods to safe methods. Theclass
folder is where the public API is implementedfor using Ruby with Rust code. These methods in theclass
folder must all be documented and tested withinthe documentation. There is a subfolder underclass
for traits calledtraits
.
Macros for abstracting away complexity are insrc/dsl.rs
.
Ruby's helper gem is in the submodule foldergem
.
Rutie will continue to be improved upon to be more and more compatible with every aspect of Ruby. Itwill also gradually change toward Rust safety, semantics, and best practices.
I imagine a future where Rutie is the stepping stone that helps Ruby switch from C to Rust.
As this package has taken 1.0 to mean both stable and safe and won't likely make a 1.0, then there canbe breaking changes expected in each MINOR version update. These MINOR version breaking changes willoccur in the public API ofsrc/class/*
andsrc/helpers/*
. For private APIs there can be breakingchanges in each PATCH version update which includessrc/rubysys/*
,src/binding/*
, andsrc/util.rs
.
If you need some more examples of usage or the git blame history please look at theRuruproject as Rutie has had the README completely rewritten and this first git commit is from Ruru.Note that there are some fundamental changes which that README won't account for.This project also hadruby-sys merged in which may have some additional beneficial git history.
Both projects that were merged into this project contained identifiers under the MIT license.This project follows with the same licensing.ruby-sys marked MIT as the license in theCargo.toml
file whereasruru had that and included a LICENSE file. This projects LICENSEhas credited the original author by preserving the MIT license author line and appending newauthor(s) which is permitted by the MIT LICENSE.
MIT LICENSE — seeLICENSE
About
“The Tie Between Ruby and Rust.”
Resources
License
Uh oh!
There was an error while loading.Please reload this page.