May 14, 2021 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.
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.
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.
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.
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();
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 |
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:
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.
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.
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.
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.
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.