如何将上下文传递给 C/C++ 中定义的 Ruby 方法

How to pass context to a Ruby method defined in C/C++

我目前正在编写一组 API 绑定到 Ruby 解释器。问题是绑定定义不是静态的,而是在初始化时检索到的。我可以轻松地即时创建类型,但为了添加方法,我需要静态处理程序:

static VALUE dispatchInstanceMethod( int argc, VALUE *argv, VALUE obj )
{...}

static VALUE dispatchClassMethod( int argc, VALUE *argv, VALUE obj )
{...}

VALUE cls  = rb_define_class_id_under( myModule, nameID, rb_cObject );
rb_define_method( cls, "methodName", RUBY_METHOD_FUNC(dispatchInstanceMethod), -1 );
rb_define_singleton_method( cls, "methodName", RUBY_METHOD_FUNC(dispatchClassMethod), -1 );

但是,这需要每个方法(静态方法和实例方法)使用不同的处理程序。 因为我在运行时生成这些定义,所以我必须使用通用调度程序来 处理所有方法调用。但是,我的调度员无法知道哪个方法和哪个 class 被称为。我怎样才能检索到这些信息?可以检索 class 信息 从传递给处理程序的 self 对象,但在静态方法的情况下 self 是 自己打字。我可以将任何类型的用户数据附加到 Class 类型 VALUE 吗?我怎么知道 如果每个方法使用相同的调度程序函数,则调用了哪个 Ruby 方法?

我过去合作过的其他一些口译员提供了 此类数据的通用 void* 指针(通常与释放函数一起)或其他一些机制,但在 Ruby 中似乎没有类似的东西。我错过了吗,还是根本无法使用?

没有什么可以阻止您通过定义 method_missing 以正常 Ruby 方式执行此操作。这为您提供了一个通用的调度程序。 Ruby 看起来像这样

class Foo
  def self.method_missing symbol, args
    # dispatch class method
  end

  def method_missing symbol, args
    # dispatch instance method
  end
end

在 C API 中,您使用 rb_define_methodrb_define_singleton_method 以正常方式执行此操作。我还没有测试过,但我想你甚至可以将同一个处理程序传递给两者并使用 self 的值来确定要执行哪个调度 (class/instance)。

这更具实验性。我不确定这将如何工作,但它基于 Ruby 的 define_method 在运行时定义正确的方法

VALUE context; // context for method call

VALUE dispatchInstanceMethod(VALUE first_arg, VALUE context, int argc, VALUE* argv)
{
    // first_arg is the first arg passed to your method, probably shouldn't use it
    // just use argc, argv instead for accessing all args
    // context should(?) be what we passed at the time of definition
}

void dynamically_create_class()
{
    VALUE cls = rb_define_class_id_under(myModule, nameID, rb_cObject);
    ID define_method = rb_intern("define_method");

    VALUE name = // method name as a Symbol object e.g. using ID2SYM
    context = // set to whatever context we need for the call
    rb_global_variable(context); // don't garbage-collect it
    rb_block_call(cls, define_method, 1, &name, dispatchInstanceMethod, context);
}

为了使它起作用(如果它能起作用),每个方法定义需要一个不同的 context 变量。这些变量的生命周期必须超过 class 的生命周期,因此它们需要在某个地方全局可用。这就是您稍后在每个方法实际调用它时区分 dispatchInstanceMethod 的方式。

这是方式在正常Ruby中更容易做到的事情。我会编写一个 C API 方法来使用基本类型(即没有自定义 classes)进行通用 API 调用。例如,类似

VALUE api_call(VALUE self, VALUE class_name, VALUE method_name, VALUE instance_variables, VALUE arguments)
{
    // turn class_name, method_name, instance_variables, arguments into an API call
}

mAPI = rb_define_module("MyAPI");
rb_define_singleton_method(mAPI, "call", api_call, 4);

然后你可以编写常规的旧 Ruby 来动态创建 class

class_def = {name: "Foo", ivars: ["bar"], methods: ["baz"]}

klass = Class.new
klass.instance_eval do
  class_def[:ivars].each do |var|
    attr_accessor var
  end

  class_def[:methods].each do |meth|
    define_method(meth) do |*args|
      ivars = Hash[instance_variables.map { |name| [name, instance_variable_get(name)] }]
      MyAPI.call(class_def[:name], meth, ivars, args)
    end
  end
end

Kernel.const_set(class_def[:name], klass)

foo = Foo.new
f.bar = 5
f.baz("Hi") # MyAPI.call("Foo", "baz", {:@bar=>5}, ["Hi"])

回答这部分问题:

And how can I know which Ruby method was called if each method uses the same dispatcher function?

您可以 caller_locations 做到这一点,并且可以 rb_eval 做到这一点。所以基本上:

VALUE myMethodName = rb_eval("caller_locations(1,1).first.label");

这将告诉您的 C 函数调用它的方法。请注意,我们不想要 caller_locations(0),因为这实际上是返回调用此方法的 ruby 代码中的 eval() 上下文,而 caller_locations(1) 是C 方法。