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 include the relevant header:
#include <libgccjit.h>
All state associated with compilation is associated with a gcc_jit_context *.
Create one using gcc_jit_context_acquire():
gcc_jit_context *ctxt;
ctxt = gcc_jit_context_acquire ();
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 gcc_jit_type *, using gcc_jit_context_get_type():
gcc_jit_type *int_type =
gcc_jit_context_get_type (ctxt, GCC_JIT_TYPE_INT);
gcc_jit_type * is an example of a “contextual” object: every entity in the API is associated with a gcc_jit_context *.
Memory management is easy: all such “contextual” objects are automatically cleaned up for you when the context is released, using gcc_jit_context_release():
gcc_jit_context_release (ctxt);
so you don’t need to manually track and cleanup all objects, just the contexts.
Although the API is C-based, there is a form of class hierarchy, which looks like this:
+- gcc_jit_object
+- gcc_jit_location
+- gcc_jit_type
+- gcc_jit_struct
+- gcc_jit_field
+- gcc_jit_function
+- gcc_jit_block
+- gcc_jit_rvalue
+- gcc_jit_lvalue
+- gcc_jit_param
There are casting methods for upcasting from subclasses to parent classes. For example, gcc_jit_type_as_object():
gcc_jit_object *obj = gcc_jit_type_as_object (int_type);
One thing you can do with a gcc_jit_object * is to ask it for a human-readable description, using gcc_jit_object_get_debug_string():
printf ("obj: %s\n", gcc_jit_object_get_debug_string (obj));
giving this text on stdout:
obj: int
This is invaluable when debugging.
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 gcc_jit_context_new_param():
gcc_jit_param *param_i =
gcc_jit_context_new_param (ctxt, NULL, int_type, "i");
Now we can create the function, using gcc_jit_context_new_function():
gcc_jit_function *func =
gcc_jit_context_new_function (ctxt, NULL,
GCC_JIT_FUNCTION_EXPORTED,
int_type,
"square",
1, ¶m_i,
0);
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:
gcc_jit_block *block = gcc_jit_function_new_block (func, NULL);
Our basic block is relatively simple: it immediately terminates by returning the value of an expression.
We can build the expression using gcc_jit_context_new_binary_op():
gcc_jit_rvalue *expr =
gcc_jit_context_new_binary_op (
ctxt, NULL,
GCC_JIT_BINARY_OP_MULT, int_type,
gcc_jit_param_as_rvalue (param_i),
gcc_jit_param_as_rvalue (param_i));
A gcc_jit_rvalue * is another example of a gcc_jit_object * subclass. We can upcast it using gcc_jit_rvalue_as_object() and as before print it with gcc_jit_object_get_debug_string().
printf ("expr: %s\n",
gcc_jit_object_get_debug_string (
gcc_jit_rvalue_as_object (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:
gcc_jit_block_end_with_return (block, NULL, expr);
OK, we’ve populated the context. We can now compile it using gcc_jit_context_compile():
gcc_jit_result *result;
result = gcc_jit_context_compile (ctxt);
and get a gcc_jit_result *.
At this point we’re done with the context; we can release it:
gcc_jit_context_release (ctxt);
We can now use gcc_jit_result_get_code() to look up a specific machine code routine within the result, in this case, the function we created above.
void *fn_ptr = gcc_jit_result_get_code (result, "square");
if (!fn_ptr)
{
fprintf (stderr, "NULL fn_ptr");
goto error;
}
We can now cast the pointer to an appropriate function pointer type, and then call it:
typedef int (*fn_type) (int);
fn_type square = (fn_type)fn_ptr;
printf ("result: %d", square (5));
result: 25
Once we’re done with the code, we can release the result:
gcc_jit_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 *)
The API is designed to cope with errors without crashing, so you can get away with having a single error-handling check in your code:
void *fn_ptr = gcc_jit_result_get_code (result, "square");
if (!fn_ptr)
{
fprintf (stderr, "NULL fn_ptr");
goto error;
}
For more information, see the error-handling guide within the Topic eference.
To get more information on what’s going on, you can set debugging flags on the context using gcc_jit_context_set_bool_option().
Setting GCC_JIT_BOOL_OPTION_DUMP_INITIAL_GIMPLE will dump a C-like representation to stderr when you compile (GCC’s “GIMPLE” representation):
gcc_jit_context_set_bool_option (
ctxt,
GCC_JIT_BOOL_OPTION_DUMP_INITIAL_GIMPLE,
1);
result = gcc_jit_context_compile (ctxt);
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 GCC_JIT_BOOL_OPTION_DUMP_GENERATED_CODE on the context before compiling:
gcc_jit_context_set_bool_option (
ctxt,
GCC_JIT_BOOL_OPTION_DUMP_GENERATED_CODE,
1);
result = gcc_jit_context_compile (ctxt);
.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 gcc_jit_context_set_int_option() with GCC_JIT_INT_OPTION_OPTIMIZATION_LEVEL:
gcc_jit_context_set_int_option (
ctxt,
GCC_JIT_INT_OPTION_OPTIMIZATION_LEVEL,
3);
.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.
Here’s what the above looks like as a complete program:
/* Usage example for libgccjit.so Copyright (C) 2014 Free Software Foundation, Inc. This file is part of GCC. GCC is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GCC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GCC; see the file COPYING3. If not see <http://www.gnu.org/licenses/>. */ #include <libgccjit.h> #include <stdlib.h> #include <stdio.h> void create_code (gcc_jit_context *ctxt) { /* Let's try to inject the equivalent of: int square (int i) { return i * i; } */ gcc_jit_type *int_type = gcc_jit_context_get_type (ctxt, GCC_JIT_TYPE_INT); gcc_jit_param *param_i = gcc_jit_context_new_param (ctxt, NULL, int_type, "i"); gcc_jit_function *func = gcc_jit_context_new_function (ctxt, NULL, GCC_JIT_FUNCTION_EXPORTED, int_type, "square", 1, ¶m_i, 0); gcc_jit_block *block = gcc_jit_function_new_block (func, NULL); gcc_jit_rvalue *expr = gcc_jit_context_new_binary_op ( ctxt, NULL, GCC_JIT_BINARY_OP_MULT, int_type, gcc_jit_param_as_rvalue (param_i), gcc_jit_param_as_rvalue (param_i)); gcc_jit_block_end_with_return (block, NULL, expr); } int main (int argc, char **argv) { gcc_jit_context *ctxt = NULL; gcc_jit_result *result = NULL; /* Get a "context" object for working with the library. */ ctxt = gcc_jit_context_acquire (); if (!ctxt) { fprintf (stderr, "NULL ctxt"); goto error; } /* Set some options on the context. Let's see the code being generated, in assembler form. */ gcc_jit_context_set_bool_option ( ctxt, GCC_JIT_BOOL_OPTION_DUMP_GENERATED_CODE, 0); /* Populate the context. */ create_code (ctxt); /* Compile the code. */ result = gcc_jit_context_compile (ctxt); if (!result) { fprintf (stderr, "NULL result"); goto error; } /* We're done with the context; we can release it: */ gcc_jit_context_release (ctxt); ctxt = NULL; /* Extract the generated code from "result". */ void *fn_ptr = gcc_jit_result_get_code (result, "square"); if (!fn_ptr) { fprintf (stderr, "NULL fn_ptr"); goto error; } typedef int (*fn_type) (int); fn_type square = (fn_type)fn_ptr; printf ("result: %d\n", square (5)); error: if (ctxt) gcc_jit_context_release (ctxt); if (result) gcc_jit_result_release (result); return 0; }
Building and running it:
$ gcc \
tut02-square.c \
-o tut02-square \
-lgccjit
# Run the built program:
$ ./tut02-square
result: 25