Engee documentation
Notebook

Translating Julia's 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

Suppose we have code on Julia that we want to port to another platform for which we are not comfortable compiling code on the platform. For example, on a microcontroller. At the same time, it would be nice to retain the ability to run the same code in a model loop inside Engee. How to proceed?

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

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

Code generation from a simple function

Let's consider generating code from a very simple function that returns us one number:

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

Declare, at the global level, two variables x1 and x2, with which we can do symbolic operations.

In [ ]:
using Symbolics
@variables x_1 x_2
Out[0]:
$$ \begin{equation} \left[ \begin{array}{c} x_{1} \\ x_{2} \\ \end{array} \right] \end{equation} $$

If we substitute the symbolic variables as arguments of our simple function, we will not see anything unusual (except that we will see the equation embedded in the function):

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

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

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

We have got code optimised for a specific "targeting" CTarget.

This code may need various modifications. For example, translation into float type or changing the names of functions (exp to expf) to meet the requirements of the standard used in your platform. Or adding the function main.

Further this code can be saved to the file simple_function.c, compiled 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

Compile the obtained code:

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

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

In [ ]:
;./out
2.909297

The result coincides with what we calculated at the beginning of the example.

Conclusion

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

It is worth noting that not all code is easily translated to C code by this method, often the translation procedure will need to be significantly modified to allow the code to accept matrices as input. But this is a way to generate a fairly large subclass of algorithms useful in a semi-natural modelling scenario.