-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial part 2: creating a trivial machine code function
Consider this C function:
int square (int i)
{
return i * i;
}
How can we construct this at run-time using libgccjit
?
First we need to open the relevant module:
open Gccjit
All state associated with compilation is associated with a Gccjit.context
.
Create one using Gccjit.Context.create ()
:
let ctx = Context.create ()
The JIT library has a system of types. It is statically-typed: every expression is of a specific type, fixed at compile-time. In our example, all of the expressions are of the C int
type, so let's obtain this from the context, as a Gccjit.type_
, using Gccjit.get_standard_type
:
let int_type = Type.(get ctx Int)
Gccjit.type_
is an example of a contextual object: every entity in the API is associated with a Gccjit.context
.
Memory management is easy: all such contextual objects are automatically cleaned up for you when the context is released, using Gccjit.Context.release
:
Context.release ctx
so you don't need to manually track and cleanup all objects, just the contexts.
Let's create the function. To do so, we first need to construct its single parameter, specifying its type and giving it a name, using Gccjit.new_param
:
let param_i = Param.create ctx int_type "i"
Now we can create the function, using Gccjit.new_function
:
let func = Function.create ctx Function.Exported int_type "square" [ param_i ]
To define the code within the function, we must create basic blocks containing statements.
Every basic block contains a list of statements, eventually terminated by a statement that either returns, or jumps to another basic block.
Our function has no control-flow, so we just need one basic block:
let block = Block.create func
Our basic block is relatively simple: it immediately terminates by returning the value of an expression.
We can build the expression using Gccjit.RValue.binary_op
:
let expr = RValue.binary_op ctx Mult int_type (RValue.param param_i) (RValue.param param_i)
As before we can print expr
with Gccjit.RValue.to_string
.
Printf.printf "expr: %s\n" (RValue.to_string expr)
giving this output:
expr: i * i
Creating the expression in itself doesn't do anything; we have to add this expression to a statement within the block. In this case, we use it to build a return statement, which terminates the basic block:
Block.return block expr
Ok, we've populated the context. We can now compile using Gccjit.Context.compile
:
let result = Context.compile ctx
and get a Gccjit.result
.
At this point we're done with the context; we can release it:
Context.release ctx
We can now use Gccjit.Result.code
to look up a specific machine code routine within the result, in this case, the function we created above.
let square = Result.code result "square" Ctypes.(int @-> returning int)
We can now call it:
Printf.printf "result: %d\n" (square 5)
result: 25
Once we're done with the code, we can release the result:
Result.release result
We can't call square
anymore once we've released result
.
Various kinds of errors are possible when using the API, such as mismatched types in an assignment. You can only compile and get code from a context if no errors occur.
Errors are printed on stderr
; they typically contain the name of the API entrypoint where the error occurred, and pertinent information on the problem:
./buggy_program: error: gcc_jit_block_add_assignment: mismatching types: assignment to i (type: int) from "hello world" (type: const char *)
On error the exception Gccjit.Error
is raised, with the api name and error string as arguments.
To get more information on what's going on, you can set debugging flags on the context using Gccjit.set_option
.
Setting Dump_initial_gimple
will dump a C-like representation to stderr
when you compile (GCC's GIMPLE representation):
Context.set_option ctx Context.Dump_initial_gimple true;
let result = Context.compile ctx in
square (signed int i)
{
signed int D.55;
<D.54>:
D.55 = i * i;
return D.55;
}
We can see the generated machine code in assembler form (on stderr
) by setting Dump_generated_code
on the context before compiling:
Context.set_option ctx Context.Dump_generated_code true;
let result = Context.compile ctx in
.text
.globl _square
.no_dead_strip _square
_square:
LFB0:
pushq %rbp
LCFI0:
movq %rsp, %rbp
LCFI1:
movl %edi, -4(%rbp)
movl -4(%rbp), %eax
imull -4(%rbp), %eax
popq %rbp
LCFI2:
ret
LFE0:
.section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
EH_frame1:
.set L$set$0,LECIE1-LSCIE1
.long L$set$0
LSCIE1:
.long 0
.byte 0x1
.ascii "zR\0"
.byte 0x1
.byte 0x78
.byte 0x10
.byte 0x1
.byte 0x10
.byte 0xc
.byte 0x7
.byte 0x8
.byte 0x90
.byte 0x1
.align 3
LECIE1:
LSFDE1:
.set L$set$1,LEFDE1-LASFDE1
.long L$set$1
LASFDE1:
.long LASFDE1-EH_frame1
.quad LFB0-.
.set L$set$2,LFE0-LFB0
.quad L$set$2
.byte 0
.byte 0x4
.set L$set$3,LCFI0-LFB0
.long L$set$3
.byte 0xe
.byte 0x10
.byte 0x86
.byte 0x2
.byte 0x4
.set L$set$4,LCFI1-LCFI0
.long L$set$4
.byte 0xd
.byte 0x6
.byte 0x4
.set L$set$5,LCFI2-LCFI1
.long L$set$5
.byte 0xc
.byte 0x7
.byte 0x8
.align 3
LEFDE1:
.subsections_via_symbols
By default, no optimizations are performed, the equivalent of GCC's -O0
option. We can turn things up to e.g. -O3
by calling Gccjit.Context.set_option
with Context.Optimization_level
.
Context.set_option ctx Context.Optimization_level 3
.section __TEXT,__text_cold,regular,pure_instructions
LCOLDB0:
.text
LHOTB0:
.align 4,0x90
.globl _square
.no_dead_strip _square
_square:
LFB0:
movl %edi, %eax
imull %edi, %eax
ret
LFE0:
.section __TEXT,__text_cold,regular,pure_instructions
LCOLDE0:
.text
LHOTE0:
.section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
EH_frame1:
.set L$set$0,LECIE1-LSCIE1
.long L$set$0
LSCIE1:
.long 0
.byte 0x1
.ascii "zR\0"
.byte 0x1
.byte 0x78
.byte 0x10
.byte 0x1
.byte 0x10
.byte 0xc
.byte 0x7
.byte 0x8
.byte 0x90
.byte 0x1
.align 3
LECIE1:
LSFDE1:
.set L$set$1,LEFDE1-LASFDE1
.long L$set$1
LASFDE1:
.long LASFDE1-EH_frame1
.quad LFB0-.
.set L$set$2,LFE0-LFB0
.quad L$set$2
.byte 0
.align 3
LEFDE1:
.subsections_via_symbols
Naturally this has only a small effect on such a trivial function.
Here's what the above looks like as a complete program:
(* Usage example for libgccjit.so *)
open Gccjit
let create_code ctx =
(* Let's try to inject the equivalent of:
int square (int i)
{
return i * i;
}
*)
let param_i = Param.create ctx Type.(get ctx Int) "i" in
let func = Function.create ctx Function.Exported Type.(get ctx Int) "square" [ param_i ] in
let block = Block.create func in
let expr = RValue.binary_op ctx Mult Type.(get ctx Int) (RValue.param param_i) (RValue.param param_i) in
Block.return block expr
let () =
let ctx = Context.create () in
(* Set some options on the context.
Let's see the code being generated, in assembler form. *)
Context.set_option ctx Context.Dump_generated_code true;
(* Populate the context. *)
create_code ctx;
(* Compile the code. *)
let result = Context.compile ctx in
(* We're done with the context; we can release it: *)
Context.release ctx;
(* Extract the generated code from "result". *)
let square = Result.code result "square" Ctypes.(int @-> returning int) in
Printf.printf "result: %d%!\n" (square 5);
Result.release result
Building and running it:
$ ocamlbuild -use-ocamlfind -package gccjit tut02_square.native
$ ./tut02_square
result: 25