Skip to content

Yadriggy C

Shigeru Chiba edited this page Feb 3, 2019 · 13 revisions

An embedded DSL similar to the C language. This DSL code is translated into C (or OpenCL) code before running to obtain better performance. In other words, this is an easy way to write a C function called from Ruby through the FFI (foreign function interface).

Overview

This DSL is used to offload computation from the Ruby VM. The code for the offloaded computation is written in this DSL but embedded in Ruby code. At runtime, the DSL processor locates the DSL code in the Ruby source code. It is parsed, translated into C code, compiled into binary on the fly by an external C compiler, and run through ruby-ffi.

Since the DSL is not Ruby, only the syntax is shared among this DSL and Ruby. In the DSL, only limited kinds of language constructs are available and the DSL code is statically typed. Neither classes or hash maps are available. The code has to be written with C-like language constructs such as functions and arrays but pointers are not supported.

Example

The following program is an example:

require 'yadriggy/c'

include Yadriggy::C::CType

def fib(n) ! Integer
  typedecl n: Integer
  if n > 1
    return fib(n - 1) + fib(n - 2)
  else
    return n
  end
end

puts Yadriggy::C.run { return fib(32) }

When this code is run, the block given to Yadriggy::C.run is translated into C code with the definition of fib method. Then the C code is compiled into a dynamic library, loaded the library through ruby-ffi, and executed. Since the block given to run calls fib, the definition of fib is also translated into C.

Although the fib method looks like a normal Ruby method (and it is normal Ruby code with respect to the syntax), it is the DSL code. ! Integer, which follows def, specifies the return type and

typedecl n: Integer

specifies the type of the parameter n. Since the DSL is a C-like language, the resulting value of the method invocation has to be explicitly returned by the return statement.

Note that the DSL performs simple type inference. Thus, you do not have to explicitly specify all the types.

For more examples, see examples.

Types

The types available in the DSL code are as follows:

DSL Type C type
Integer int32_t
Int int32_t
Float double
Float32 float
Void void
String char*
IntArray int32_t[]
FloatArray double[]
Float32Array float[]

arrayof(Integer), arrayof(Float), and arrayof(Float32) are aliases of IntArray, FloatArray, and Float32Array, respectively.

Literals

Only numbers and simple strings are valid literals. Either symbols, arrays, or hashes are not valid.

Type declaration

The return type is specified by ! followed by a type name. It has to be written at the same line as def's. The parameter types has to be declared in the next line by typedecl.

typedecl a: Int, b: Float

This declares that the variable a has type Int and the variable b has type Float. A local variable type has to be declared in another typedecl, which may be at the third line or later.

Operators

The binary operators available in the DSL code are +, -, *, /, %, <, >, <=, >=, ==, &&, and ||.

-@ (unary minus) is the only unary operator available.

=, +=, -=, and so on are also available for assignment.

an array access such as a[i] and assignment to an array such as a[i] = 3 are also supported.

Statements

Unlike Ruby, when a function returns a value, it has to be explicitly returend by the return statement. The resulting value of the last-executed expression is not considered as a return value.

if-elsif-else and while statements are available. Ternary if (?:) and if modifier are also available.

for statement is also supported but the range has to be a range literal in integer. For example,

for i in 0...n
  b[i] = a[i] + 1
end

is valid. ... and .. are supported. The operands have to be an integer literal or variable. An array cannot be used as a range.

For looping, the times method is available when the receiver is an integer literal or variable. The example above can be also written as follows:

n.times do |i|
  b[i] = a[i] + 1
end

Althoguh it seems that the block is passed to times as in Ruby, times is a special form in this DSL; yield or Proc is not available. The call to the times method is translated into a for statement in C.

Array

An array object cannot be created in the DSL. All the array objects used in the DSL code have to be created in Ruby code and explicitly passed as an argument to a function written in the DSL. The elements of the arrays are shared among Ruby code and the DSL code. Such an array is not a regular array object; it has to be an instance of IntArray, FloatArray, or Float32Array. The following code is an example:

arr = IntArray.new(5)
arr[0] = 1
puts Yadriggy::C.run { return foo(arr) }
puts a_out.to_a

The IntArray object is passed to the DSL code at the 3rd line. The elements of an IntArray object can be accessed through [] as the elements of a regular array. They are converted into a regualr array by the to_a method.

The following methods on the array objects are available in Ruby (they are not available within the DSL code):

Methods available in Ruby description
to_a() returns an Array object containing the elements.
size() returns the number of the elements.
length() returns the number of the elements.
set_values sets the i-th element to the value of the given block `{

Foreign function calls

The DSL code can call a function contained in the C libraries.

def exp(f)
  typedecl f: Float, foreign: Float
  Math.exp(f)
end

If typedecl has an argument foreign:, a call to the function (for example, exp) is translated into a call to the corresponding C function with the same name. The return type is specified by foreign: (for example, Float). Since the function body is ignored, it can be blank but writing the implementation in Ruby is useful to express the behavior of the function. It also allows a call to the function as a Ruby method.

Specifying the function body in C by a string literal is also possible.

def current_time() ! Int
  typedecl native: "struct timespec time;\n\
clock_gettime(CLOCK_MONOTONIC, &time);\n\
return time.tv_sec * 1000000 + time.tv_nsec / 1000;"
  Time.now * 1000000
end

If typedecl has an argument native:, the function body is specified by the string literal given by this argument. The function body written in Ruby

  Time.now * 1000000

is ignored. It can be blank.

Object orientation

Object orientation is not supported since this DSL is a C-like language. The object creation by new, instance variables such as @field, or class variables such as @@cvar are not supported.

However, a function body written in the DSL can read an instance variable as well as free variables and self. It is not permitted to assign a new value to the instance variable or free variables. The value of the instance variable is treated as a constant value and thus its copy is embedded into the generated C code. When a method is invoked on that value, a method call is translated into a direct invocation of the method that value provides.

Compile and run

The simplest way to run the DSL code is to call Yadriggy::C.run:

Yadriggy::C.run { return fib(32) }

This translates the DSL code in the block into C code, compiles it, and runs it. The method invoked in the body is also part of the DSL code. The method is considered as a function defined in that DSL code.

When the DSL code is repeatedly called with a different argument, define a class inheriting from Yadriggy::C::Program. Its constructor must not take any arguments. Suppose that we define the Fib class as such a class. Then,

m = Fib.compile('CFib', 'fib', dir: './tmp')

This translates all the public methods in the Fib class into C functions and compiles them. All the arguments are optional. The compile method generates a shared library named libfib.so (after the second argument 'fib'). Then it returns a Module object where the Ruby methods for invoking the C functions are defined. For example,

puts m.fib(32)

this invokes the fib function written in the Fib class. All the generated files during the compilation are stored in ./tmp.

If the first argument to compile is not nil, then it generates a Ruby program to load the library later. When the program is executed, the module named by the first argument CFib is defined. It contains the Ruby methods for invoking the C functions.

For complete source code, see examples.

Built-in functions

In the Yadriggy::C::Program, the following functions are defined and available in its subclasses:

name description
printf(format, ...) the printf function in the standard C library.
current_time() gets the current time in msec. The return type is Int.
sqrtf(f) gets the square root of f in the single precision.
sqrt(f) gets the square root of f in the double precision.
expf(f) gets the base-e exponential of f in the single precision.
exp(f) gets the base-e exponential of f in the double precision.
logf(f) gets the the natural logarithm of f in the single precision.
log(f) gets the the natural logarithm of f in the double precision.

Header file

Yadriggy::C::Config specifies various settings. To add a header file to the generated C code,

Yadriggy::C::Config::Headers << '#include <stdlib.h>'