Storing C data in an MRuby class
Tags: ruby, c, tutorial
Published: 2020-05-30 04:35:30

Quick Friday hit for the 0 people following along my project and the dozen people discovering this years later from Google.

There aren't any good examples on creating a Ruby class with mruby that encapsulates data not used within the VM. This is something you need when you're using mruby to interface with a C library that provides it's own data types. You want to call methods in Ruby which run C code calling out to the external library. Examples include database systems, graphics APIs and interacting with the operating system.

It's a different though simpler process with mruby than mainline Ruby. C Ruby requires a separate `allocate` method before `initialize`. With mruby we can perform all operations with creating and storing C data all within the `initialize` method.

We'll use a contrived `Foo_data` struct.

```c++
struct Foo_data {
  uint32_t first[16];
  uint32_t second[16];
};
```

For demonstration we'll create a constructor for a `Foo` class taking one integer parameter. This parameter will instruct how to fill out the `first` and `second` members of a `Foo_data` struct that's a part of an instance of `Foo`. Building a Ruby `Foo` class in C like usual

```c++
mrb_state *state = mrb_open();
RClass *Foo = mrb_define_class(state, "Foo", state->object_class);
MRB_SET_INSTANCE_TT(Foo, MRB_TT_DATA);

mrb_define_method(state, Foo, "initialize", Foo_initialize, MRB_ARGS_REQ(1));
mrb_define_method(state, Foo, "check", Foo_check, MRB_ARGS_NONE());
```

There's a _highly_ important new thing here. The `MRB_SET_INSTANCE_TT` sets the class object table type to the one specified. In this case we're setting this to a special `MRB_TT_DATA`. I neglected to do this step and encountered erratic difficult to debug program behavior. The program would follow pointers to nowhere and quit outright, not throwing the normal Access Violation error normally seen when following wild pointers.

Next we create a `mrb_data_type`. This is a struct which informs the mruby VM of the data type and the function to call when the GC comes for the object. It has two members, name and a function pointer. Name is a unique `char*` identifier, this is unique and identifies the data type. The name of the class, `"Foo"` is fine. The function pointer is what the GC calls when it destroys the object. If you're not doing anything special you can use the default built in function `mrb_free`

```c++
static const mrb_data_type Foo_type = {
  "Foo", mrb_free
};
```

Filling out the `initialize` method. We'll allocate a new `Foo_data` and save it to the instance on `initialize`. 

```c++
mrb_value
Foo_initialize(mrb_state* state, mrb_value self) {
  mrb_int n;
  mrb_get_args(state, "i", &n);
  Foo_data *foo = (Foo_data *)DATA_PTR(self);
  if(foo) { mrb_free(state, foo); }
  mrb_data_init(self, nullptr, &Foo_type);
  foo = (Foo_data *)malloc(sizeof(Foo_data));

  for(uint32_t i = 0; i < 16; ++i) {
    foo->first[i] = i * n;
    foo->second[i] = i * n * n;
  }

  mrb_data_init(self, foo, &Foo_type);
  return self;
}
```

Explaining a couple things here. `DATA_PTR` pulls a `void *` out from the object specified, in this case `self`. We then cast it to what we want, a `Foo_data *`. For reasons I don't entirely understand, though recommended [in this discussion](https://github.com/mruby/mruby/issues/9#issuecomment-147070792) and used in the [mruby time gem](https://github.com/mruby/mruby/blob/cc1db2d1172de6187467372ad6f175eccdd97d59/mrbgems/mruby-time/src/time.c#L751) we see if there already is a pointer associated with the instance. If so we free it.

We call `mrb_data_init` first to initialize the `void *` destined for holding the `Foo_data *`. Then a normal C heap allocation, and then filling out the data with the integer passed into the constructor. Calling `mrb_data_init` with the populated `Foo_data *` saves the data to the instance.

We extract it again at `check`

```c++
mrb_value
Foo_check(mrb_state* state, mrb_value self) {
  Foo_data *foo;
  Data_Get_Struct(state, self, &Foo_type, foo);
  mrb_assert(foo != nullptr);
  return mrb_nil_value();
}
```

`Data_Get_Struct` is a macro which will pull out and type cast the `void *` saved to the instance. All of the definitions and implementations are in `data.h` and `data.c` in the mruby code.

Now all that's left is create instances of `Foo` with different data and confirm with a debugger that the data is what we expect.

```c++
mrb_load_string(state, "a = Foo.new(10); a.check; b = Foo.new(50); b.check");
```

![](https://dev-to-uploads.s3.amazonaws.com/i/a1jyne8nwzyul2dnv2hg.png)

![](https://dev-to-uploads.s3.amazonaws.com/i/g1fdvc47ovgd822h7h9e.png)

And that's it! It seems daunting at first understanding how to create a class within mruby for saving arbitrary C data with the lack of information. I hope this information makes the process clear and helps someone wanting to do the same.

The full program

```c++
#include <stdlib.h>
#include "mruby.h"
#include "ext\mruby\data.h"
#include "ext\mruby\class.h"
#include "ext\mruby\compile.h"

struct Foo_data {
  uint32_t first[16];
  uint32_t second[16];
};

static const mrb_data_type Foo_type = {
  "Foo", mrb_free
};

mrb_value
Foo_initialize(mrb_state* state, mrb_value self) {
  mrb_int n;
  mrb_get_args(state, "i", &n);
  Foo_data *foo = (Foo_data *)DATA_PTR(self);
  if(foo) { mrb_free(state, foo); }
  mrb_data_init(self, nullptr, &Foo_type);
  foo = (Foo_data *)malloc(sizeof(Foo_data));

  for(uint32_t i = 0; i < 16; ++i) {
    foo->first[i] = i * n;
    foo->second[i] = i * n * n;
  }

  mrb_data_init(self, foo, &Foo_type);
  return self;
}

mrb_value
Foo_check(mrb_state* state, mrb_value self) {
  Foo_data *foo;
  Data_Get_Struct(state, self, &Foo_type, foo);
  mrb_assert(foo != nullptr);
  return mrb_nil_value();
}

int main() {
  mrb_state *state = mrb_open();
  RClass *Foo = mrb_define_class(state, "Foo", state->object_class);
  MRB_SET_INSTANCE_TT(Foo, MRB_TT_DATA);

  mrb_define_method(state, Foo, "initialize", Foo_initialize, MRB_ARGS_REQ(1));
  mrb_define_method(state, Foo, "check", Foo_check, MRB_ARGS_NONE());

  mrb_load_string(state, "a = Foo.new(10); a.check; b = Foo.new(50); b.check");
  return 0;
}
```