Creating a trivial machine code function — gccjit 0.4 documentation (original) (raw)

Consider this C function:

int square(int i) { return i * i; }

How can we construct this from within Python using libgccjit?

First we need to import the Python bindings to libgccjit:

All state associated with compilation is associated with agccjit.Context:

ctxt = gccjit.Context()

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:

int_type = ctxt.get_type(gccjit.TypeKind.INT)

The various objects in the API have reasonable __str__ methods:

Let’s create the function. To do so, we first need to construct its single parameter, specifying its type and giving it a name:

param_i = ctxt.new_param(int_type, b'i') print(param_i) i

Now we can create the function:

fn = ctxt.new_function(gccjit.FunctionKind.EXPORTED, ... int_type, # return type ... b"square", # name ... [param_i]) # params print(fn) square

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:

block = fn.new_block(b'entry') print(block) entry

Our basic block is relatively simple: it immediately terminates by returning the value of an expression. We can build the expression:

expr = ctxt.new_binary_op(gccjit.BinaryOp.MULT, ... int_type, ... param_i, param_i) print(expr) i * i

This 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.end_with_return(expr)

OK, we’ve populated the context. We can now compile it:

jit_result = ctxt.compile()

and get a gccjit.Result.

We can now look up a specific machine code routine within the result, in this case, the function we created above:

void_ptr = jit_result.get_code(b"square")

We can now use ctypes.CFUNCTYPE to turn it into something we can call from Python:

import ctypes int_int_func_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int) callable = int_int_func_type(void_ptr)

It should now be possible to run the code:

Options

To get more information on what’s going on, you can set debugging flags on the context using gccjit.Context.set_bool_option().

Setting gccjit.BoolOption.DUMP_INITIAL_GIMPLE will dump a C-like representation to stderr when you compile (GCC’s “GIMPLE” representation):

ctxt.set_bool_option(gccjit.BoolOption.DUMP_INITIAL_GIMPLE, True) jit_result = ctxt.compile() square (signed int i) { signed int D.260;

entry: D.260 = i * i; return D.260; }

We can see the generated machine code in assembler form (on stderr) by setting gccjit.BoolOption.DUMP_GENERATED_CODE on the context before compiling:

ctxt.set_bool_option(gccjit.BoolOption.DUMP_GENERATED_CODE, True) jit_result = ctxt.compile() .file "fake.c" .text .globl square .type square, @function square: .LFB6: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, -4(%rbp) .L14: movl -4(%rbp), %eax imull -4(%rbp), %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE6: .size square, .-square .ident "GCC: (GNU) 4.9.0 20131023 (Red Hat 0.2-0.5.1920c315ff984892399893b380305ab36e07b455.fc20)" .section .note.GNU-stack,"",@progbits

By default, no optimizations are performed, the equivalent of GCC’s-O0 option. We can turn things up to e.g. -O3 by callinggccjit.Context.set_int_option() withgccjit.IntOption.OPTIMIZATION_LEVEL:

ctxt.set_int_option(gccjit.IntOption.OPTIMIZATION_LEVEL, 3) jit_result = ctxt.compile() .file "fake.c" .text .p2align 4,,15 .globl square .type square, @function square: .LFB7: .cfi_startproc .L16: movl %edi, %eax imull %edi, %eax ret .cfi_endproc .LFE7: .size square, .-square .ident "GCC: (GNU) 4.9.0 20131023 (Red Hat 0.2-0.5.1920c315ff984892399893b380305ab36e07b455.fc20)" .section .note.GNU-stack,"",@progbits

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:

import ctypes

import gccjit

def create_fn():

Create a compilation context:

ctxt = gccjit.Context()

Turn these on to get various kinds of debugging:

if 0: ctxt.set_bool_option(gccjit.BoolOption.DUMP_INITIAL_TREE, True) ctxt.set_bool_option(gccjit.BoolOption.DUMP_INITIAL_GIMPLE, True) ctxt.set_bool_option(gccjit.BoolOption.DUMP_GENERATED_CODE, True)

Adjust this to control optimization level of the generated code:

if 0: ctxt.set_int_option(gccjit.IntOption.OPTIMIZATION_LEVEL, 3)

int_type = ctxt.get_type(gccjit.TypeKind.INT)

Create parameter "i":

param_i = ctxt.new_param(int_type, b'i')

Create the function:

fn = ctxt.new_function(gccjit.FunctionKind.EXPORTED, int_type, b"square", [param_i])

Create a basic block within the function:

block = fn.new_block(b'entry')

This basic block is relatively simple:

block.end_with_return( ctxt.new_binary_op(gccjit.BinaryOp.MULT, int_type, param_i, param_i))

Having populated the context, compile it.

jit_result = ctxt.compile()

This is what you get back from ctxt.compile():

assert isinstance(jit_result, gccjit.Result)

return jit_result

def test_calling_fn(i): jit_result = create_fn()

Look up a specific machine code routine within the gccjit.Result,

in this case, the function we created above:

void_ptr = jit_result.get_code(b"square")

Now use ctypes.CFUNCTYPE to turn it into something we can call

from Python:

int_int_func_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int) code = int_int_func_type(void_ptr)

Now try running the code:

return code(i)

if name == 'main': print(test_calling_fn(5))