Write Ruby methods using C (Numo :: NArray)

(Added on August 29, 2020) When passing an instance of Numo :: DFloat as an argument Numo::DFloat(view)#shape= It may not be possible to pass correctly when it is in view like. This happens when you cut out a part of the array. In that case, if you copy it with dup and then pass it, there will be no problem. It doesn't seem to work just by casting.

Introduction

Please refer to "Writing Ruby methods using C ++" for how to write a Ruby extension library in C ++. I think that you can create a library with almost no effort as long as you write C ++. Here, I will introduce 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 just a normal C function. Let's create an extension library that can be called from Ruby using SWIG. A function that returns twice 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 to create a header file separately, so I am doing so. 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's just reading 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

You will have testF.bundle (extension depends on the OS). Test program

test1.rb


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

In the above example, only one return value can be taken, but multiple return values are possible by using pointer arguments. Please refer to it as it is written in the SWIG manual. * Search with OUTPUT to find out 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's quite difficult to use.

How to use Numo :: NArray

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

A function that takes an array and calculates the sum, and a function that calculates the double value. int n is the number of elements in the array and double x [] is the 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 [] { So, it is applied to the argument that matches double [] in the header file (test2.h), double ?? []. Here, I'm just passing a pointer to the data in NArray to the function, I'm not checking the data type at all, and I'm only passing the size of the array as int n, so if I make a mistake, I'll definitely get an error. Become. Returning data with double y []. It's a pretty rough method, but it's a level where 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 numa / narray.h, so please rewrite it according to your environment. Don't make a mistake as it is located in the num folder that contains this, not 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 on the size of the array, so I improved it to use it. It also casts the data type to Numo :: DFloat. (The data type to be cast is specified by numa_cDFloat) Here, instead of reading with% include as before, it is written directly at the bottom of test.i.

There are two% typemap (in), but the first one double sum_d(int SIZE, double *NARRAY); Matches. The second one is void twicefold_dv(int SIZE, double *NARRAY_in, double *NARRAY_out); I try to match. This is further improved to create an NArray for the return value and 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 of temp here is converted to temp1, we expect it to be temp1 in% typemap (argout). It's a pretty dangerous way of writing, but I couldn't find a good way, so it was a compromise. If that causes an error, rewrite it. You can see it by looking at the contents of test_wrap.c.

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

There is no change in other files, so you can use it as it is. The command is 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

Recommended Posts

Write Ruby methods using C (Numo :: NArray)
Write Ruby methods using C (Part 1)
Write Ruby methods using C ++ (Part 2) Benchmark
Write code using Ruby classes and instances
Ruby Learning # 15 Methods
About Ruby methods
Try using java.lang.Math methods
Ruby Learning # 31 Object Methods
About Ruby instance methods
Ruby variables and methods
[Ruby] methods, instance methods, etc ...
What you write in C # is written in Ruby like this
I wrote a C parser (like) using PEG in Ruby