-
Notifications
You must be signed in to change notification settings - Fork 2
Yadriggy C
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).
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.
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: Integerspecifies 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.
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.
Only numbers and simple strings are valid literals. Either symbols, arrays, or hashes are not valid.
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: FloatThis 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.
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.
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
endis 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
endAlthoguh 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.
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_aThe 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 `{ |
The DSL code can call a function contained in the C libraries.
def exp(f)
typedecl f: Float, foreign: Float
Math.exp(f)
endIf 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
endIf 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 * 1000000is ignored. It can be blank.
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.
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.
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. |
Yadriggy::C::Config specifies various settings.
To add a header file to the generated C code,
Yadriggy::C::Config::Headers << '#include <stdlib.h>'