Engee documentation
Notebook

Translating Julia functions into C code

In this example, we will demonstrate one technique that allows you to generate C code from a function created in Julia.

Task description

Let's assume that we have code on Julia that we want to migrate to another platform for which it is inconvenient for us to compile code on the platform. For example, on a microcontroller. At the same time, it would be good to keep the ability to run the same code in the model loop inside Engee. What should I do?

Let's use the polymorphism mechanism. You can feed numbers into your code, and then it will execute and calculate a numerical result for you. You can submit matrices, then the result will be a matrix. Let's input symbolic variables and see what happens.

In [ ]:
Pkg.add( "Symbolics" )

Code generation from a simple function

Consider generating code from a very simple function that returns us a single number.:

In [ ]:
simple_fcn(x1, x2) = 2 .* x1 + sin( x2 )
simple_fcn(1, 2)
Out[0]:
2.909297426825682

Let's declare, at the global level, two variables x1 and x2, with which symbolic operations can be performed.

In [ ]:
using Symbolics
@variables x_1 x_2
Out[0]:
$$ \begin{equation} \left[ \begin{array}{c} \mathtt{x\_1} \\ \mathtt{x\_2} \\ \end{array} \right] \end{equation} $$

If we substitute symbolic variables as arguments to our simple function, we won't see anything unusual (except that we'll see the equation embedded in the function):

In [ ]:
simple_fcn( x_1, x_2 )
Out[0]:
$$ \begin{equation} 2 \mathtt{x\_1} + \sin\left( \mathtt{x\_2} \right) \end{equation} $$

But we will get a well-structured object from which we can generate code.:

In [ ]:
c_model_code = build_function( simple_fcn( x_1, x_2 ), [x_1, x_2]; target=Symbolics.CTarget(), fname="SIMPLE_FCN", lhsname=:c, rhsnames=[:x] )
println( c_model_code )
#include <math.h>
void SIMPLE_FCN(double* c, const double* x) {
  c[0] = 2 * x[0] + sin(x[1]);
}

We have received a code optimized for a specific "target". CTarget.

This code may require various improvements. For example, converting to type float or changing function names (exp on expf) to meet the requirements of the standard used in your platform. Or adding a function main.

Then this code can be saved to a file. simple_function.c, compile with the command gcc -o out simple_function.c -lm.

In [ ]:
c_code_standalone = """
#include <stdio.h>

$c_model_code

int main(int argc, char const *argv[]){
    double out[1];
    double in[] = {1, 2};

    SIMPLE_FCN( out, in );

    printf( "%f\\n", out[0] );
    return 0;
}""";

# Сохраняем в файл доработанный код
open("$(@__DIR__)/simple_function.c", "w") do f
    println( f, "$c_code_standalone" )
end

Compiling the resulting code:

In [ ]:
;gcc -o out simple_function.c -lm

And check that the resulting binary file outputs the same value as the original function.:

In [ ]:
;./out
2.909297
In [ ]:
simple_fcn(1, 2)
Out[0]:
2.909297426825682

The result is the same as what we calculated at the beginning of the example.

Conclusion

We generated the code for a very simple function on Julia, compiled and executed it in Engee, or we could have put it in a block. C Function to simplify semi-natural tests.

It is worth noting that not every code is easily translated into C code by this method, often the translation procedure will need to be significantly changed so that it allows the code to accept matrices as input. But this way it is possible to generate a fairly large subclass of algorithms that are useful in a semi-natural simulation scenario.