-
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: 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.
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: 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.
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
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.
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 `{ |
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 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>'