Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Julia Embedded Julia


May 14, 2021 Julia


Table of contents


Embedded Julia

We already know that calling C and Fortran code Julia can call C functions in a simple and efficient way. B ut there are many cases where the opposite is true: the Julia function needs to be called from C. T his consolidates Julia's code into larger C/C?projects without having to rewrite everything in C/C++. J ulia provides an API for C to do this. Just as most languages have methods to call C functions, Julia's API can also be used to bridge bridges with other languages.

Advanced embedding

Let's start with a simple C program that initializes Julia and calls some of Julia's code: :

  #include <julia.h>

  int main(int argc, char *argv[])
  {
      jl_init(NULL);
      JL_SET_STACK_BASE;

      jl_eval_string("print(sqrt(2.0))");

      return 0;
  }

To compile this program you need to include Julia's header file in the path and link the library libjulia For example, if Julia $JULIA_DIR you can compile it with gcc:

    gcc -o test -I$JULIA_DIR/include/julia -L$JULIA_DIR/usr/lib -ljulia test.c

Or take a look at the embedding system under embedding.c example/

Initialize Julia before calling the Julia function, which jl_init and the parameter of this function is the Julia installation path, the type is const char* I f there are no parameters, Julia automatically looks for Julia's installation path.

The second statement initializes Julia's task scheduling system. T his statement must appear in a function that does not return, as long as Julia is called main well). S trictly speaking, this statement is optional, but the operation of the conversion task will cause problems if it is omitted.

The third statement in the tester jl_eval_string Julia jl_eval_string the call to the tester.

Type conversion

A real application not only needs to execute an expression, but also returns the value of the main program. jl_eval_string jl_value_t which is a pointer to the Julia object assigned on the heap. S toring simple data types such as Float64 is boxing and extracting the original data stored is unboxing The square root of Julia Calculated 2 that we promoted and the sample program that reads the results in C are as follows:

    jl_value_t *ret = jl_eval_string("sqrt(2.0)");

    if (jl_is_float64(ret)) {
        double ret_unboxed = jl_unbox_float64(ret);
        printf("sqrt(2.0) in C: %e \n", ret_unboxed);
    }

To check ret is a specified Julia type, we can jl_is_... function. B y typeof(sqrt(2.0)) the Julia shell, we can see that the return type is Double in Float64 (C). In order to convert the value of the installed Julia into double in the C jl_unbox_float64 is used in the code snippet above.

The jl_box_... is used to convert in another way:

    jl_value_t *a = jl_box_float64(3.0);
    jl_value_t *b = jl_box_float32(3.0f);
    jl_value_t *c = jl_box_int32(3);

As we'll see below, calling the Julia function boxing with the specified parameters is required.

Call Julia's function

When jl_eval_string C language to get the results of a Julia expression, it does not allow parameters evaluated in C to be passed to Julia. For this, you need to call the Julia function directly, jl_call :

    jl_function_t *func = jl_get_function(jl_base_module, "sqrt");
    jl_value_t *argument = jl_box_float64(2.0);
    jl_value_t *ret = jl_call1(func, argument);

In the first step, the handling of sqrt is jl_get_function sqrt. T he first argument jl_get_function is a pointer to Base module, where sqrt defined. T he double value is then jl_box_float64 value. F inally, in the last step, the function is jl_call1 function. jl_call0 jl_call2 jl_call2 jl_call3 jl_call3 also exist to easily handle the number of different parameters. To pass more parameters, jl_call :

    jl_value_t *jl_call(jl_function_t *f, jl_value_t **args, int32_t nargs)

The second argument, args is jl_value_t* and the nargs of the argument.

Memory management

As we've seen, the Julia object is rendered in C as a pointer. This raises the question of who should release these objects.

Typically, the Julia object is released by a garbage collector (GC), but the GC does not automatically know that we have a reference to the Julia value in C. This means that the GC can release the pointer and invalidate it.

GC can only run when Julia objects are assigned. C alls jl_box_float64 assignments, and allocations can occur in any pointer running Julia code. I t jl_... pointers between the ...... calls. B ut in order to confirm that jl_... survive... the call survives, we have to tell Julia that we have a reference to the Julia value. T his can be JL_GC_PUSH macro instructions in the . This can be done using the JL_GC_PUSH macros:

    jl_value_t *ret = jl_eval_string("sqrt(2.0)");
    JL_GC_PUSH1(&ret);
    // Do something with ret
    JL_GC_POP();

JL_GC_POP releases a JL_GC_PUSH established. Note JL_GC_PUSH stack, so it must accurately and accurately group with the JL_GC_POP frame JL_GC_POP destroyed.

Several Julia values can JL_GC_PUSH2 JL_GC_PUSH3 JL_GC_PUSH4 instructions are immediately pushed. For push an array of Julia values we can JL_GC_PUSHARGS macro instruction, which can be used as follows to: cro, which can be used as follows:

    jl_value_t **args;
    JL_GC_PUSHARGS(args, 2); // args can now hold 2 `jl_value_t*` objects
    args[0] = some_value;
    args[1] = some_other_value;
    // Do something with args (e.g. call jl_... functions)
    JL_GC_POP();

Control garbage collection

There are some functions that control the GC. In normal use cases, these should not be required.

void jl_gc_collect() Force a GC run
void jl_gc_disable() Disable the GC
void jl_gc_enable() Enable the GC

Process the array

Julia and C can share array data without copying. The next example will show that this works.

The Julia array is displayed in jl_array_t* type, the data type. Basically, jl_array_t is a structure that contains the following:

  • Information about the data type
  • A pointer to a block of data
  • Information about the size of the array

To keep things simple, let's start with a 1D array. Creating a Float64 array with 10 elements in length is done by:

    jl_value_t* array_type = jl_apply_array_type(jl_float64_type, 1);
    jl_array_t* x          = jl_alloc_array_1d(array_type, 10);

Or, if you have assigned an array you can generate a simple wrapper of the data:

double *existingArray = (double*)malloc(sizeof(double)*10);
jl_array_t *x = jl_ptr_to_array_1d(array_type, existingArray, 10, 0);

The last argument is a Boolean value that indicates whether Julia should take ownership of the data. If this argument is non-zero, GC calls free on the data pointer when the array is no longer free

In order to get x's data, we jl_array_data :

    double *xData = (double*)jl_array_data(x);

Now we can fill in the array:

    for(size_t i=0; i<jl_array_len(x); i++)
        xData[i] = i;

Now let's call a x that runs the operation on x:

    jl_function_t *func  = jl_get_function(jl_base_module, "reverse!");
    jl_call1(func, (jl_value_t*)x);

By printing the array, we can verify that the elements of x are now reversed.

Access the returned array

If a Julia function returns an array, jl_eval_string and jl_call can be converted jl_array_t* :

    jl_function_t *func  = jl_get_function(jl_base_module, "reverse");
    jl_array_t *y = (jl_array_t*)jl_call1(func, (jl_value_t*)x);

y content can now jl_array_data using the content. As always, make sure to keep a reference to the array when it is in use.

High-dimensional array

Julia's multi-dimensional array is stored in memory in the order of columns. Here's some code for creating two-dimensional arrays and getting properties:

    // Create 2D array of float64 type
    jl_value_t *array_type = jl_apply_array_type(jl_float64_type, 2);
    jl_array_t *x  = jl_alloc_array_2d(array_type, 10, 5);

    // Get array pointer
    double *p = (double*)jl_array_data(x);
    // Get number of dimensions
    int ndims = jl_array_ndims(x);
    // Get the size of the i-th dim
    size_t size0 = jl_array_dim(x,0);
    size_t size1 = jl_array_dim(x,1);

    // Fill array with data
    for(size_t i=0; i<size1; i++)
        for(size_t j=0; j<size0; j++)
            p[j + size0*i] = i + j;

Notice that when the Julia array uses an index of 1-based, the API of C uses an index of 0-base jl_array_dim is called) to read as the usual C code.

Abnormal

Julia code can throw exceptions. For example, consider the following:

      jl_eval_string("this_function_does_not_exist()");

This call will do nothing. However, it is possible to check whether an exception is thrown.

    if (jl_exception_occurred())
        printf("%s \n", jl_typeof_str(jl_exception_occurred()));

If you use the Julia C API in a language that supports exceptions (e.g., Python, C, C, plus), it makes sense to wrap each call libjulia with a function that checks if the exception has been thrown, and it rethrows the exception in the main language.

Throw a Julia exception

When writing a callable Julia function, it is necessary to validate the arguments and throw exceptions to indicate the error. A typical type check looks like this:

    if (!jl_is_float64(val)) {
        jl_type_error(function_name, (jl_value_t*)jl_float64_type, val);
    }

A common exception can be caused by using a function:

    void jl_error(const char *str);
    void jl_errorf(const char *fmt, ...);

jl_error to use a C string, jl_errorf is called like printf

    jl_errorf("argument x = %d is too large", x);

In this example x assumed to be an integer.