[Ruby] Write a Ruby method using C (Numo::NArray)

5 minute read

(Added 08/20/2020) When passing an instance of Numo::DFloat as an argument, Numo::DFloat(view)#shape= When it is a view like, it may not be passed correctly. This happens when you cut out a part of the array. At that time, if you pass it after copying with dup, it will be no problem. It doesn’t seem to work just by casting.

Introduction

Please refer to “Writing Ruby method using C++” (https://qiita.com/ki073/items/b35e1e0ecc4862ea3c14)” for how to write the extension library of Ruby in C++. I think that you can create a library with almost no difficulty if you only write C++. Here, I will show you how to make a function written in C into an extension library.

Passing to a simple variable

As a simple example, I wrote a function that passes a simple variable. It’s an ordinary C function. Let’s make an extension library that can be called from Ruby using SWIG. A function that returns double the value and a function that returns the length of the string.

test1.h


# include <string.h>

double twicefold_d(double x);
int string_length(char* str);

test1.c


# include "test1.h"

double twicefold_d(double x){
  return(x*2.0);
}

int string_length(char* str){
  return(strlen(str));
}

It is convenient because it is convenient to make a header file separately. The second function takes a string as an argument. Next, create a file for SWIG. testF of module is the module name when calling from Ruby (however, the first character is converted to uppercase). It just reads the header file.

test.i


%module testF
%{
# include "test1.h"
%}

%include test1.h

extconf.rb


require'mkmf'
create_makefile("testF")

Put the above files in the same folder and execute the following command.

swig -ruby test.i
ruby extconf.rb
make

testF.bundle (extension depends on OS) is completed. Test program

test1.rb


require "./testF"
p TestF.twicefold_d(3.4)
p TestF.string_length("abcd")

In the above example, you can get only one return value, but you can also use multiple return values by using a pointer argument. Refer to the SWIG manual for reference. *Searching for “OUTPUT” will show you how to do it.

When passing an array

There are several ways to pass an array. The manual describes how to use carray.i, but it is quite difficult to use.

How to use Numo::NArray

Numo::NArray is capable of high-speed processing, so I think that many people are using it. I will show you how to pass it to a function written in C via it.

A function that receives an array and calculates the sum and a function that calculates the doubled value. int n is the number of elements in the array, and double x[] is a pointer to the array.

test2.h


double sum_d(int n, double x[]);
void twicefold_dv(int n, double x[], double y[]);

test2.c


# include "test2.h"

double sum_d(int n, double x[]){
  double sum=0;
  for(int i=0; i<n; i++){
    sum +=x[i];
  }
  return(sum);
}


void twicefold_dv(int n, double x[], double y[]){
  for(int i=0; i<n; i++){
    y[i]=x[i]*2.0;
  }
}

test.i


%module testF
%{
# include "numo/narray.h"
# include "test2.h"
%}

%typemap(in) double [] {
  narray_t *nary;
  GetNArray($input, nary);
  $1 = ($1_ltype)na_get_pointer_for_read($input);
}
%include test2.h

%typemap(in) double [] { Then, it is designed to be applied to three places of the double ??[] argument that matches the double [] in the header file (test2.h). Here, I’m just passing a pointer to the data in the NArray to the function, I’m not checking the data type at all, and I’m only passing the array size as an int n, so I’m sure I get an error if I make a mistake. Become. Data is returned in double y[]. It’s a pretty violent method, but it is a level that you can pass array data anyway.

extconf.rb


equire'mkmf'
dir_config("numo-narray")
create_makefile("testF")

A second line has been added to get the position of numo/narray.h.

swig -ruby test.i
ruby extconf.rb ----with-numo-narray-include=/opt/local/lib/ruby2.7/gems/2.7.0/gems/numo-narray-0.9.1.8/lib/numo/
make

The second line tells you the location of numo/narray.h, so please rewrite it according to your environment. Please be careful not to make a mistake because it is located in the numo folder that contains this instead of narray.h.

test2.rb


require "numo/narray"
require "./testF"

p TestF.sum_d(4, Numo::DFloat.cast([1.0,2.1,3.2,4.3]))
result= Numo::DFloat.zeros(4)
TestF.twicefold_dv(4, Numo::DFloat.cast([1.0,2.1,3.2,4.3]), result)
p result

revised edition

The NArray data itself has information about the size of the array, so it was improved to use it. It also casts the data type to Numo::DFloat. (The data type to be cast is specified by numo_cDFloat) Here, instead of reading with %include as before, we write directly under test.i.

There are two %typemap(in), the first one is double sum_d(int SIZE, double *NARRAY); Matches The second one void twicefold_dv(int SIZE, double *NARRAY_in, double *NARRAY_out); I try to match. Here, I am creating an NArray for the return value and improving it to return it.

test.i


%module testF
%{
# include "numo/narray.h"
# include "test2.h"
%}

%typemap(in) (int SIZE, double *NARRAY) {
  narray_t *nary;
  VALUE obj1;
  obj1 = rb_funcall(numo_cDFloat, rb_intern("cast"), 1, $input);
  GetNArray(obj1, nary);
  $2 = ($2_ltype)na_get_pointer_for_read(obj1);
  $1 = NA_SHAPE(nary)[0];
}

%typemap(in) (int SIZE, double *NARRAY_in, double *NARRAY_out) (VALUE temp) {
  narray_t *nary1, *nary2;
  VALUE obj1, obj2;
  size_t obj2_shape[1];
  obj1 = rb_funcall(numo_cDFloat, rb_intern("cast"), 1, $input);
  GetNArray(obj1, nary1);
  $2 = ($2_ltype)na_get_pointer_for_read(obj1);
  obj2_shape[0] = NA_SHAPE(nary1)[0];
  obj2 = rb_funcall(nary_new(numo_cDFloat, 1, obj2_shape), rb_intern("fill"), 1, INT2FIX(0));
  temp=obj2;
  GetNArray(obj2, nary2);
  $3 = ($3_ltype)na_get_pointer_for_read(obj2);
  $1 = NA_SHAPE(nary1)[0];
}

%typemap(argout) (int SIZE, double *NARRAY_in, double *NARRAY_out){
  $result=temp1;
}

double sum_d(int SIZE, double *NARRAY);
void twicefold_dv(int SIZE, double *NARRAY_in, double *NARRAY_out);

It is designed to return the array calculated by %typemap(argout), but it is not linked with temp in %typemap(in). Since the variable name has been converted to temp1 in temp here, I expect it to be temp1 in %typemap(argout). It’s a pretty dangerous way of writing, but I couldn’t find a good method, so it was a compromise. If it causes an error, please rewrite. You can see it by looking at the contents of test_wrap.c.

I have not checked the dimensions of the array, but there is information in narray.h, so please add it if necessary.

You can use it as it is because other files are not changed. The commands are exactly the same.

test3.rb


require "numo/narray"
require "./testF"

p TestF.sum_d(Numo::DFloat.cast([1.0,2.1,3.2,4.3]))
p TestF.twicefold_dv(Numo::DFloat.cast([1.0,2.1,3.2,4.3])))

p TestF.sum_d(Numo::SFloat.cast([1.0,2.1,3.2,4.3]))

#### **`macOS 10.13.6 Ruby 2.7.1`**

SWIG 4.0.2