Tutorial part 3: Loops and variables#
Consider this C function:
int loop_test (int n) { int sum = 0; for (int i = 0; i < n; i++) sum += i * i; return sum; }
This example demonstrates some more features of libgccjit, with local variables and a loop.
To break this down into libgccjit terms, it’s usually easier to reword the for loop as a while loop, giving:
int loop_test (int n) { int sum = 0; int i = 0; while (i < n) { sum += i * i; i++; } return sum; }
Here’s what the final control flow graph will look like:
As before, we include the libgccjit header and make a gcc_jit_context*.
#include <libgccjit.h>
void test (void)
{
gcc_jit_context *ctxt;
ctxt = gcc_jit_context_acquire ();
The function works with the C int type:
gcc_jit_type *the_type =
gcc_jit_context_get_type (ctxt, GCC_JIT_TYPE_INT);
gcc_jit_type *return_type = the_type;
though we could equally well make it work on, say, double:
gcc_jit_type *the_type =
gcc_jit_context_get_type (ctxt, GCC_JIT_TYPE_DOUBLE);
Let’s build the function:
gcc_jit_param *n =
gcc_jit_context_new_param (ctxt, NULL, the_type, "n");
gcc_jit_param *params[1] = {n};
gcc_jit_function *func =
gcc_jit_context_new_function (ctxt, NULL,
GCC_JIT_FUNCTION_EXPORTED,
return_type,
"loop_test",
1, params, 0);
Expressions: lvalues and rvalues#
The base class of expression is the gcc_jit_rvalue*, representing an expression that can be on the right-hand side of an assignment: a value that can be computed somehow, and assigned to a storage area (such as a variable). It has a specific gcc_jit_type*.
Another important class is gcc_jit_lvalue*. A gcc_jit_lvalue*. is something that can of the left-hand side of an assignment: a storage area (such as a variable).
In other words, every assignment can be thought of as:
LVALUE = RVALUE;
Note that gcc_jit_lvalue* is a subclass of gcc_jit_rvalue*, where in an assignment of the form:
LVALUE_A = LVALUE_B;
the LVALUE_B implies reading the current value of that storage area, assigning it into the LVALUE_A.
So far the only expressions we’ve seen are i * i:
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));
which is a gcc_jit_rvalue*, and the various function parameters: param_i and param_n, instances of gcc_jit_param*, which is a subclass of gcc_jit_lvalue* (and, in turn, of gcc_jit_rvalue*): we can both read from and write to function parameters within the body of a function.
Our new example has a couple of local variables. We create them by
calling gcc_jit_function_new_local()
, supplying a type and a
name:
/* Build locals: */
gcc_jit_lvalue *i =
gcc_jit_function_new_local (func, NULL, the_type, "i");
gcc_jit_lvalue *sum =
gcc_jit_function_new_local (func, NULL, the_type, "sum");
These are instances of gcc_jit_lvalue* - they can be read from and written to.
Note that there is no precanned way to create and initialize a variable like in C:
int i = 0;
Instead, having added the local to the function, we have to separately add an assignment of 0 to local_i at the beginning of the function.
Control flow#
This function has a loop, so we need to build some basic blocks to handle the control flow. In this case, we need 4 blocks:
before the loop (initializing the locals)
the conditional at the top of the loop (comparing i < n)
the body of the loop
after the loop terminates (return sum)
so we create these as gcc_jit_block* instances within the gcc_jit_function*:
gcc_jit_block *b_initial =
gcc_jit_function_new_block (func, "initial");
gcc_jit_block *b_loop_cond =
gcc_jit_function_new_block (func, "loop_cond");
gcc_jit_block *b_loop_body =
gcc_jit_function_new_block (func, "loop_body");
gcc_jit_block *b_after_loop =
gcc_jit_function_new_block (func, "after_loop");
We now populate each block with statements.
The entry block b_initial consists of initializations followed by a jump
to the conditional. We assign 0 to i and to sum, using
gcc_jit_block_add_assignment()
to add
an assignment statement, and using gcc_jit_context_zero()
to get
the constant value 0 for the relevant type for the right-hand side of
the assignment:
/* sum = 0; */
gcc_jit_block_add_assignment (
b_initial, NULL,
sum,
gcc_jit_context_zero (ctxt, the_type));
/* i = 0; */
gcc_jit_block_add_assignment (
b_initial, NULL,
i,
gcc_jit_context_zero (ctxt, the_type));
We can then terminate the entry block by jumping to the conditional:
gcc_jit_block_end_with_jump (b_initial, NULL, b_loop_cond);
The conditional block is equivalent to the line while (i < n) from our
C example. It contains a single statement: a conditional, which jumps to
one of two destination blocks depending on a boolean
gcc_jit_rvalue*, in this case the comparison of i and n.
We build the comparison using gcc_jit_context_new_comparison()
:
/* (i >= n) */
gcc_jit_rvalue *guard =
gcc_jit_context_new_comparison (
ctxt, NULL,
GCC_JIT_COMPARISON_GE,
gcc_jit_lvalue_as_rvalue (i),
gcc_jit_param_as_rvalue (n));
and can then use this to add b_loop_cond’s sole statement, via
gcc_jit_block_end_with_conditional()
:
/* Equivalent to:
if (guard)
goto after_loop;
else
goto loop_body; */
gcc_jit_block_end_with_conditional (
b_loop_cond, NULL,
guard,
b_after_loop, /* on_true */
b_loop_body); /* on_false */
Next, we populate the body of the loop.
The C statement sum += i * i; is an assignment operation, where an
lvalue is modified “in-place”. We use
gcc_jit_block_add_assignment_op()
to handle these operations:
/* sum += i * i */
gcc_jit_block_add_assignment_op (
b_loop_body, NULL,
sum,
GCC_JIT_BINARY_OP_PLUS,
gcc_jit_context_new_binary_op (
ctxt, NULL,
GCC_JIT_BINARY_OP_MULT, the_type,
gcc_jit_lvalue_as_rvalue (i),
gcc_jit_lvalue_as_rvalue (i)));
The i++ can be thought of as i += 1, and can thus be handled in
a similar way. We use gcc_jit_context_one()
to get the constant
value 1 (for the relevant type) for the right-hand side
of the assignment.
/* i++ */
gcc_jit_block_add_assignment_op (
b_loop_body, NULL,
i,
GCC_JIT_BINARY_OP_PLUS,
gcc_jit_context_one (ctxt, the_type));
Note
For numeric constants other than 0 or 1, we could use
gcc_jit_context_new_rvalue_from_int()
and
gcc_jit_context_new_rvalue_from_double()
.
The loop body completes by jumping back to the conditional:
gcc_jit_block_end_with_jump (b_loop_body, NULL, b_loop_cond);
Finally, we populate the b_after_loop block, reached when the loop conditional is false. We want to generate the equivalent of:
return sum;
so the block is just one statement:
/* return sum */
gcc_jit_block_end_with_return (
b_after_loop,
NULL,
gcc_jit_lvalue_as_rvalue (sum));
Note
You can intermingle block creation with statement creation, but given that the terminator statements generally include references to other blocks, I find it’s clearer to create all the blocks, then all the statements.
We’ve finished populating the function. As before, we can now compile it to machine code:
gcc_jit_result *result;
result = gcc_jit_context_compile (ctxt);
typedef int (*loop_test_fn_type) (int);
loop_test_fn_type loop_test =
(loop_test_fn_type)gcc_jit_result_get_code (result, "loop_test");
if (!loop_test)
goto error;
printf ("result: %d", loop_test (10));
result: 285
Visualizing the control flow graph#
You can see the control flow graph of a function using
gcc_jit_function_dump_to_dot()
:
gcc_jit_function_dump_to_dot (func, "/tmp/sum-of-squares.dot");
giving a .dot file in GraphViz format.
You can convert this to an image using dot:
$ dot -Tpng /tmp/sum-of-squares.dot -o /tmp/sum-of-squares.png
or use a viewer (my preferred one is xdot.py; see https://github.com/jrfonseca/xdot.py; on Fedora you can install it with yum install python-xdot):
Full example#
/* Usage example for libgccjit.so Copyright (C) 2014-2022 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) { /* Simple sum-of-squares, to test conditionals and looping int loop_test (int n) { int i; int sum = 0; for (i = 0; i < n ; i ++) { sum += i * i; } return sum; */ gcc_jit_type *the_type = gcc_jit_context_get_type (ctxt, GCC_JIT_TYPE_INT); gcc_jit_type *return_type = the_type; gcc_jit_param *n = gcc_jit_context_new_param (ctxt, NULL, the_type, "n"); gcc_jit_param *params[1] = {n}; gcc_jit_function *func = gcc_jit_context_new_function (ctxt, NULL, GCC_JIT_FUNCTION_EXPORTED, return_type, "loop_test", 1, params, 0); /* Build locals: */ gcc_jit_lvalue *i = gcc_jit_function_new_local (func, NULL, the_type, "i"); gcc_jit_lvalue *sum = gcc_jit_function_new_local (func, NULL, the_type, "sum"); gcc_jit_block *b_initial = gcc_jit_function_new_block (func, "initial"); gcc_jit_block *b_loop_cond = gcc_jit_function_new_block (func, "loop_cond"); gcc_jit_block *b_loop_body = gcc_jit_function_new_block (func, "loop_body"); gcc_jit_block *b_after_loop = gcc_jit_function_new_block (func, "after_loop"); /* sum = 0; */ gcc_jit_block_add_assignment ( b_initial, NULL, sum, gcc_jit_context_zero (ctxt, the_type)); /* i = 0; */ gcc_jit_block_add_assignment ( b_initial, NULL, i, gcc_jit_context_zero (ctxt, the_type)); gcc_jit_block_end_with_jump (b_initial, NULL, b_loop_cond); /* if (i >= n) */ gcc_jit_block_end_with_conditional ( b_loop_cond, NULL, gcc_jit_context_new_comparison ( ctxt, NULL, GCC_JIT_COMPARISON_GE, gcc_jit_lvalue_as_rvalue (i), gcc_jit_param_as_rvalue (n)), b_after_loop, b_loop_body); /* sum += i * i */ gcc_jit_block_add_assignment_op ( b_loop_body, NULL, sum, GCC_JIT_BINARY_OP_PLUS, gcc_jit_context_new_binary_op ( ctxt, NULL, GCC_JIT_BINARY_OP_MULT, the_type, gcc_jit_lvalue_as_rvalue (i), gcc_jit_lvalue_as_rvalue (i))); /* i++ */ gcc_jit_block_add_assignment_op ( b_loop_body, NULL, i, GCC_JIT_BINARY_OP_PLUS, gcc_jit_context_one (ctxt, the_type)); gcc_jit_block_end_with_jump (b_loop_body, NULL, b_loop_cond); /* return sum */ gcc_jit_block_end_with_return ( b_after_loop, NULL, gcc_jit_lvalue_as_rvalue (sum)); } 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; } /* Extract the generated code from "result". */ typedef int (*loop_test_fn_type) (int); loop_test_fn_type loop_test = (loop_test_fn_type)gcc_jit_result_get_code (result, "loop_test"); if (!loop_test) { fprintf (stderr, "NULL loop_test"); goto error; } /* Run the generated code. */ int val = loop_test (10); printf("loop_test returned: %d\n", val); error: gcc_jit_context_release (ctxt); gcc_jit_result_release (result); return 0; }
Building and running it:
$ gcc \
tut03-sum-of-squares.c \
-o tut03-sum-of-squares \
-lgccjit
# Run the built program:
$ ./tut03-sum-of-squares
loop_test returned: 285