Tutorial part 2: creating a trivial machine code function - nojb/ocaml-gccjit GitHub Wiki

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.

Error-handling

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.

Options

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.

Full example

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
⚠️ **GitHub.com Fallback** ⚠️