A Rubyist's Walk Along the C-side (Part 2): Defining Methods

A Rubyist's Walk Along the C-side (Part 2): Defining Methods

This is an article in a multi-part series called “A Rubyist’s Walk Along the C-side”

In the previous article, you saw how to set up and write your very first C extension. In this article, we’ll explore how to define Ruby methods.

Defining methods 

The simplest way to define a method is to use the rb_define_method class of functions. These functions require four arguments:

  1. klass: The class on which you are defining the method on.
  2. name: The name of the method as a C string.
  3. func: The implementation function (described in further detail below).
  4. argc: The number of arguments your Ruby method accepts. You can pass in -1 for a variable number of arguments in a C array, and -2 for a variable number of arguments in a Ruby array.

The function signature for rb_define_method looks like the following.

void rb_define_method(VALUE klass, const char *name,
                      VALUE (*func)(ANYARGS), int argc);

For example,

// Defines method `my_method` in the Integer class with 2 arguments
rb_define_method(rb_cInteger, "my_method", my_method, 2);

In addition to rb_define_method, you can also use the following functions to define methods (you use these the same way as rb_define_method):

  • rb_define_protected_method: Defines a protected method.
  • rb_define_private_method: Defines a private method.
  • rb_define_singleton_method: Defines a method on the singleton class.

Implementing methods with a fixed number of arguments 

To define an implementation function for a Ruby method with a known number of arguments, you can directly specify the arguments in the function. The first argument is always self, which is the object the method is being called on. The function must also have a return value, which is the value returned by the Ruby method. All of these are of type VALUE (which, if you recall from part 1, is the type used to represent Ruby objects in C). An example is shown below.

VALUE my_method(VALUE self, VALUE arg1, VALUE arg2)
{
    // Your implementation goes here
}

Implementing methods with a variable number of arguments 

Defining methods with a variable number of arguments is a little more tricky. There are two ways to do this, using a C array and the other using a Ruby array (which one you use is determined by whether you pass -1 or -2 to argc in rb_define_method).

Option 1: C array 

To get the arguments in a C array, you must pass -1 to argc when calling rb_define_method. The function will accept three arguments and return a VALUE that is returned by the Ruby method. The arguments are as follows:

  1. argc: The number of arguments passed in.
  2. argv: Pointer to C array of arguments.
  3. self: The object this method is called on.
VALUE my_method(int argc, VALUE* argv, VALUE self)
{
    // Your implementation goes here
}

Option 2: Ruby array 

To get the arguments in a Ruby array, you must pass -2 to argc when calling rb_define_method. The function will accept two arguments and return a VALUE that is returned by the Ruby method. The arguments are as follows:

  1. self: The object this method is called on.
  2. args: The arguments in a Ruby array.

We haven’t discussed how to read a Ruby array in C which will be covered in a later article (of course, a perfectly valid way is to just call the Array#[] method, but there’s a more efficient way).

VALUE my_method(VALUE self, VALUE args)
{
    // Your implementation goes here
}

Blocks 

To yield a block passed into our method, you can use the rb_yield_values function inside our method. rb_yield_values will accept the number of arguments, followed by a list of arguments, and will return the return value of the block.

// Yielding block with no arguments
VALUE block_ret = rb_yield_values(0);

// Yielding block with two arguments
VALUE block_ret = rb_yield_values(2, val1, val2);

Checking for blocks 

Sometimes we don’t always want to yield the block, but only when it’s present. Just like how we can use Kernel#block_given? in Ruby code, we can use the corresponding rb_block_given_p function in C.

if (rb_block_given_p()) {
    // Block given
} else {
    // No block given
}

Requiring blocks 

If we call yield in Ruby or rb_yield_values without a block present, we get a LocalJumpError: no block given. This is usually fine, but sometimes we want to be more defensive and check that a block is passed in earlier in the code, especially if we do mutations before we call yield and the exception might cause us to enter a bad state. To check that the block exists, there’s a handy function rb_need_block that will raise a LocalJumpError if no block is passed in. The implementation is fairly simple, so you should be able to understand more or less what it’s doing with your current knowledge!

Putting it all together 

You can find the accompanying source code in the GitHub repository peterzhu2118/ruby-c-ext-code

Here we see all the ways to define Ruby methods. We define a helper function kernel_puts that calls Kernel#puts on the Ruby object passed in. We also define Object#my_fixed_args_methodObject#my_var_args_c_array_method, and Object#my_var_args_rb_array_method methods that show the three ways of defining Ruby methods using C. These methods prints self on the first line, followed by the arguments on each line. We also define a method Object#my_method_with_required_block that demos how to yield a block and capture the return value of the block. You can see a demo of these methods in methods.rb.

#include <ruby.h>

static ID id_puts;

static void kernel_puts(VALUE val)
{
    rb_funcall(rb_mKernel, id_puts, 1, val);
}

static VALUE my_fixed_args_method(VALUE self, VALUE arg1, VALUE arg2)
{
    kernel_puts(self);
    kernel_puts(arg1);
    kernel_puts(arg2);

    return Qnil;
}

static VALUE my_var_args_c_array_method(int argc, VALUE* argv, VALUE self)
{
    kernel_puts(self);

    for (int i = 0; i < argc; i++) {
        kernel_puts(argv[i]);
    }

    return Qnil;
}

static VALUE my_var_args_rb_array_method(VALUE self, VALUE args)
{
    kernel_puts(self);
    kernel_puts(args);

    return Qnil;
}

static VALUE my_method_with_required_block(VALUE self)
{
    VALUE block_ret = rb_yield_values(0);
    kernel_puts(block_ret);

    return Qnil;
}

void Init_methods(void)
{
    id_puts = rb_intern("puts");

    rb_define_method(rb_cObject, "my_fixed_args_method", my_fixed_args_method, 2);
    rb_define_method(rb_cObject, "my_var_args_c_array_method", my_var_args_c_array_method, -1);
    rb_define_method(rb_cObject, "my_var_args_rb_array_method", my_var_args_rb_array_method, -2);

    rb_define_method(rb_cObject, "my_method_with_required_block", my_method_with_required_block, 0);
}

Conclusion 

In this article, you saw how to implement Ruby methods using the C API. You saw the basic way of defining methods with a static number of parameters and the two ways to define methods with a dynamic number of parameters. You also saw how to handle blocks in methods. In the next article, we’ll take a more in-depth look at how to call Ruby methods using the C API.

posted @   unicornsir  阅读(71)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示