mRuby:mrbgems以及在Ruby对象中储存struct

mRuby拥有自己的包管理系统,mrbgems。与gem不同的是,mrbgems静态的,是在编译mruby时发挥作用的,而rubygems则是在ruby编译好以后可以进行动态的扩展。

mrbgems

首先我们来看一下官方给出的样例mrbgem

#include <mruby.h>
#include <stdio.h>

static mrb_value
mrb_c_method(mrb_state *mrb, mrb_value self)
{
puts("A C Extension");
return self;
}

void
mrb_c_extension_example_gem_init(mrb_state* mrb) {
struct RClass *class_cextension = mrb_define_module(mrb, "CExtension");
mrb_define_class_method(mrb, class_cextension, "c_method", mrb_c_method, MRB_ARGS_NONE());
}

void
mrb_c_extension_example_gem_final(mrb_state* mrb) {
// finalizer
}

来源于examples\mrbgems\cextensionexample\src\example.c

mrbgem要求你定义mrb\_YOURGEMNAME\_gem\_init(mrb\_state\* mrb)函数以及mrb\_YOURGEMNAME\_gem\_final(mrb\_state\* mrb)函数,分别会在加载与结束的时候被调用。当然即使没有什么内容一个空函数也是不能少的。其中函数的内容就和我们在上一节中所使用的差不多。

下面,我们来看看mrbgem.rake

MRuby::Gem::Specification.new('c\_extension\_example') do |spec|
  spec.license = 'MIT'
  spec.author  = 'mruby developers'
end

其中cextensionexample是你的gem的名字。license是许可证,author是作者,感觉说的好像有点多……具体自己参考这里吧,其对gem的路径格式也有定义。

创建完以后,在mruby的根目录下的build\_config.rb中加一行conf.gem 'mrbgems/mruby-c-extension'当然要记得指向你自己的gem地址,我就不多说了。

最后make一下,编译过程中没错误的话就可以用了。在将其嵌入到程序之前,你可以使用mirb先测试一下。

使用C struct

有时候我们可能会需要使用Ruby变量来储存一些数据,比如C的struct。在一个Ruby对象中储存struct有两种方法,一种是用对象本身包装成一个C Struct。我个人认为这个方法更加麻烦一点,而且可扩展性也不算太高……不能让一个对象拥有两个struct之类……另一种方法是为该对象定义一个实例变量(instance variable),用这个实例变量包装struct。

用实例本身包装

#include "mruby.h"
#include "mruby/compile.h"
#include "mruby/data.h"
#include "mruby/class.h"
#include "mruby/variable.h"
#include <stdlib.h>
#include <stdio.h>
struct Foo {
  int a;
};
static void foo\_free(mrb\_state\* mrb,void\* p){
  free(p);
  printf("Freed\n");
}
static struct mrb\_data\_type foo\_data\_type = {
  "foo", foo\_free
};
mrb\_value new\_foo(mrb\_state\* mrb,mrb\_value self){
  struct Foo\* f;
  f = (struct Foo\*)DATA\_PTR(self);
  if(f){
    mrb\_free(mrb,f);
  }
  DATA\_TYPE(self) = &foo\_data\_type;
  DATA\_PTR(self) = NULL;
  f  = (struct Foo\*)mrb\_malloc(mrb,sizeof(struct Foo));
  f->a = 0;
  DATA\_PTR(self) = f;
  return self;
}
mrb\_value set\_foo\_a(mrb\_state\* mrb,mrb\_value self){
  int v=0;
  mrb\_get\_args(mrb,"i",&v);
  struct Foo\* f;
  f = DATA\_GET\_PTR(mrb,self,&foo\_data\_type,struct Foo);
  f->a = v;
  return mrb\_nil\_value();
}
mrb\_value get\_foo\_a(mrb\_state\* mrb,mrb\_value self){
  struct Foo\* f;
  f = DATA\_GET\_PTR(mrb,self,&foo\_data\_type,struct Foo);
  return mrb\_fixnum\_value(f->a);
}

void mrb_mruby_c_extension_gem_init(mrb_state* mrb) {
struct RClass* module_test = mrb_define_module(mrb,"Test");
struct RClass* class_foo = mrb_define_class_under(mrb,module_test,"Foo",mrb->object_class);
MRB_SET_INSTANCE_TT(class_foo, MRB_TT_DATA);
mrb_define_method(mrb,class_foo,"initialize",new_foo,MRB_ARGS_NONE());
mrb_define_method(mrb,class_foo,"a",get_foo_a,MRB_ARGS_NONE());
mrb_define_method(mrb,class_foo,"a=",set_foo_a,MRB_ARGS_REQ(1));
}
void mrb_mruby_c_extension_gem_final(mrb_state* mrb) {
/*Do nothing*/
}

这部分的代码主要参考mruby-time这个gem的代码。首先来看一下new\_foo

首先应该指出的一点就是这里的initialize是一个实例方法,所以说传给new\_fooselfFoo.new生成的是实例自身而不是Foo这个Class类的实例常量

Ruby中内建的类型有一种DATA原型,该原型有一个DATA指针用来储存数据内容,所以说我们可以通过使用该DATA指针来储存struct。不过在此之前,先通过MRB\_SET\_INSTANCE\_TT(class\_foo, MRB\_TT\_DATA)将Foo类的所有实例设为DATA原型。

然后我们就可以通过DATA\_PTR(self)获取实例的DATA指针。注意:DATA\_PTR(self)宏实际上等于((struct RData \*)mrb\_ptr(self))->data,并不是一个字面值常量,所以对其进行赋值是可行的。

在取得DATA指针以后要对其进行释放,然后再将其指向自己创建的新内存中,这部分的代码全部都在new\_foo函数中。关于mrb\_free,目前从源码来看其与free没有区别,但是不保证以后会有什么修改,所以我个人还是倾向Ruby的东西用mrb\_free,自己定义的东西用free。

关于mrb\_data\_type,在这里Matz本人阐述了其作用,主要是Debug,以及定义了一个Data要如何被释放掉。正如代码所示,该struct由一段任意的字符串作为名字以及一个函数指针作为释放时使用的函数组成。

当然,为了让mRuby了解到该实例的类型,也要通过DATA\_TYPE(self) = &foo\_data\_type指定一下实例的类型。

编译后,在mirb下使用如下代码测试

> s = Test::Foo.new
 => #<Test::Foo:0x6d3d30>
> s.a
 => 0
> s.a = 10
 => 10
> s.a
 => 10
> s = 20
 => 20
> GC.start
Freed
 => nil

用实例变量包装

#include "mruby.h"
#include "mruby/compile.h"
#include "mruby/data.h"
#include "mruby/class.h"
#include "mruby/variable.h"
#include <stdlib.h>
#include <stdio.h>
struct Foo {
  int a;
};
static void foo\_free(mrb\_state\* mrb,void\* p){
  free(p);
  printf("Freed\n");
}
static struct mrb\_data\_type foo\_data\_type = {
  "foo", foo\_free
};
mrb\_value new\_foo(mrb\_state\* mrb,mrb\_value self){
  struct Foo\* f;
  f  = (struct Foo\*)mrb\_malloc(mrb,sizeof(struct Foo));
  f->a = 0;
  mrb\_iv\_set(mrb,self,mrb\_intern\_cstr(mrb,"@data"),
  mrb\_obj\_value(Data\_Wrap\_Struct(mrb,mrb->object\_class,&foo\_data\_type,(void \*)f)));
  return self;
}
mrb\_value set\_foo\_a(mrb\_state\* mrb,mrb\_value self){
  int v=0;
  mrb\_get\_args(mrb,"i",&v);
  struct Foo\* f;
  Data\_Get\_Struct(mrb,mrb\_iv\_get(mrb,self,mrb\_intern\_cstr(mrb,"@data")),&foo\_data\_type,f);
  f = (struct Foo \*)f;
  f->a = v;
  return mrb\_nil\_value();
}
mrb\_value get\_foo\_a(mrb\_state\* mrb,mrb\_value self){
  struct Foo\* f;
  Data\_Get\_Struct(mrb,mrb\_iv\_get(mrb,self,mrb\_intern\_cstr(mrb,"@data")),&foo\_data\_type,f);
  f = (struct Foo \*)f;
  return mrb\_fixnum\_value(f->a);
}

void mrb_mruby_c_extension_gem_init(mrb_state* mrb) {
struct RClass* module_test = mrb_define_module(mrb,"Test");
struct RClass* class_foo = mrb_define_class_under(mrb,module_test,"Foo",mrb->object_class);
mrb_define_method(mrb,class_foo,"initialize",new_foo,MRB_ARGS_NONE());
mrb_define_method(mrb,class_foo,"a",get_foo_a,MRB_ARGS_NONE());
mrb_define_method(mrb,class_foo,"a=",set_foo_a,MRB_ARGS_REQ(1));
}
void mrb_mruby_c_extension_gem_final(mrb_state* mrb) {
/*Do nothing*/
}

在这里,我们定义了一个实例变量@data,用其存储struct数据。该变量通过Data\_Wrap\_Struct(mrb,mrb->object\_class,&foo\_data\_type,(void \*)f)中的mrb->object\_class被指定为了Object的实例。

我们使用了两个宏,Data\_Wrap\_Struct以及Data\_Get\_Struct,前者返回一个指定类的Data原型的实例,另一个则是从指定的对象中取得数据指针,它与DATAGETPTR是一模一样的,而与DATA\_PTR相比多了一个对类型的检测。

Data_Get_Struct已经废弃,请使用DATA_GET_PTR替代

mrb\_intern\_cstr会返回一个可供Ruby使用的标识符,其参数应该不用我多说。

mrb\_iv\_setmrb\_iv\_get的声明如下:

void mrb\_vm\_iv\_set(mrb\_state\*, mrb\_sym, mrb\_value);

mrb\_value mrb\_iv\_get(mrb\_state \*mrb, mrb\_value obj, mrb\_sym sym);

应该比较容易理解吧?

其他好像也没什么需要具体讲解的内容了,直接编译试试效果吧。

> s = Test::Foo.new
 => #<Test::Foo:0x903d30 @data=#<Object:0x903d18>>
> s.instance\_variables
 => [:@data]
> s.instance\_eval{@data}.class
 => Object
> s.instance\_eval{@data}.methods - Object.methods
 => []
> s.a = 10
 => 10
> s
 => #<Test::Foo:0x903d30 @data=#<Object:0x903d18>>
> s.a
 => 10
> s.instance\_eval{@data = 0}
 => 0
> s.a
(mirb):1: wrong argument type Fixnum (expected Data) (TypeError)
> s.a=102
(mirb):1: wrong argument type Fixnum (expected Data) (TypeError)
> s = 3
 => 3
> GC.start
Freed
 => nil

可以看出GC的确在好好工作。顺便如果不想让@data被修改的话可以把@data命名为data,这样它就不会被访问到了。

总结

这两种方法虽然实现方式不同,但是原理上是差不多的,都是通过DATA原型提供的DATA指针来存储数据,将struct用Ruby对象包装起来使用。不同的只是包装用的Ruby对象是什么、属于哪个类的实例而已而已。顺带一提,如果你想的话,你也可以把Data\_Wrap\_Struct中的mrb->object\_class改成其他的类,甚至是Foo类自身都可以。