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))