Creating a trivial machine code function¶
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:
>>> import gccjit
All state associated with compilation is associated with a
gccjit.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:
>>> print(int_type)
int
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:
>>> callable(5)
25
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 calling
gccjit.Context.set_int_option()
with
gccjit.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))