d-unsed / ruru

Native Ruby extensions written in Rust
MIT License
832 stars 40 forks source link

Splat of parameters from Ruby (variadic function) #90

Open danielpclark opened 6 years ago

danielpclark commented 6 years ago

It would be nice to either have a dynamic size of arguments like when using splat operators. We can either have it auto-converted into an Array or create a new type for iterating called Splat or Args.

Currently trying to use Array for an unknown size of parameters doesn't work.

I imagine specifying in the methods! macro that the Splat type will produce an iterator to sequentially produce Value objects from argc and argv.

Okay I believe I found the relevant info. What we want is rb_scan_args — Ruby C Extensions Part 6: Defining Functions. Additional info on it at The Definitive Guide to Ruby's C API. Helix has it already rb_scan_args

This debug code for scan_args shows the many ways to use it: scan_args/scan_args.c and includes how to make and Array from it return rb_ary_new_from_values(numberof(args), args) and -ext-/test_scan_args.rb shows the Ruby test side for those C methods.

danielpclark commented 6 years ago

Alright, I figured out how to use rb_scan_args in Rust. It's pretty simple. You give it a mapping of how the parameters will be laid out as a string and then everything after that is reference pointers to Value objects you've instantiated in Rust.

let args: [Value; 2] = [ 
  Value::from(0),
  Value::from(0)
];  

unsafe {
  util::rb_scan_args(
    argc, // Argc
    argv, // *const Value
    str_to_cstring("1*").as_ptr(),
    &args[0],
    &args[1]
  );  
}

let path_self = AnyObject::from(args[0]);
let args_array = Array::from(args[1]);

rb_scan_args replaces the values I established in the beginning of the code block. I haven't accounted for exceptions being raised in this though.

Since the argv parameter of rb_scan_args takes a *const Value as ruby-sys doesn't contain the AnyObject type I had to cordon off a custom CallbackPtr as a ValueCallbackPtr and re-implement a define_singleton_method parameters to take my type. This helped me avoid the needles conversions into and out of AnyObject steps for rb_scan_args.

What I really would like to do is have the methods! macro detect the method parameters and create a custom string mapping for rb_scan_args so we can use splatted arguments anywhere. If we get exception handling covered with it as well it would be an added “type” verification for arguments 😉😉.

danielpclark commented 6 years ago

Excerpt from The Definitive Guide to Ruby's C API for the string mapping:

Parsing Arguments

Well, if you accept a variable number of arguments you could code all of that logic yourself in the method, and make it behave like it has a fancier method definition in Ruby. Thankfully, the API has a shortcut for doing exactly that. To use it, you should use the C array function definition, then you can pass argc and argv along to:

int rb_scan_args(int argc, const VALUE* argv, const char* fmt, ...);

Here fmt is a format string describing how the method arguments would look in Ruby. The string can have at most 6 characters, where each character describes a different section of the arguments. The six sections and their corresponding characters are (in order):

  1. The number of leading mandatory arguments: a digit
  2. The number of optional arguments: a digit
  3. A splatted argument: *
  4. The number of trailing mandatory arguments: a digit
  5. Keyword arguments: :
  6. A block argument: &

Each section is optional, so you can leave out the characters for things you don’t need. Be aware that the parsing of the format string is greedy: 1* describes a method with one mandatory argument and a splat. If you want one optional argument and a splat you must specify 01*. Following the format string, you must pass a VALUE* for each Ruby argument. The number of pointers passed should equal the “total” of the six sections, though you can pass NULL for an argument you don’t care about. For example the format string 21*& should have 5 VALUE*s passed (2 mandatory, 1 optional, 1 splatted, 1 block).

rb_scan_args() unpacks argv using the VALUE*s you pass it and will raise a fitting exception if the wrong number of arguments were passed.

danielpclark commented 6 years ago

I have working splat code now in my master branch of FasterPath, although it's just for one method. You can see it here: https://github.com/danielpclark/faster_path/blob/v0.2.5/src/pathname_sys.rs#L23-L36