Using Julia with Nim

In this tutorial, we explore how to use Nimjl to integrate Julia code with Nim.

What is Julia ?

Julia is a dynamically typed scripting language designed for high performance; it compiles to efficient native code through LLVM.

Most notably, it has a strong emphasis on scientific computing and Julia's Array types are one of the fastest multi-dimensional arrays - or Tensor-like - data structures out there.

Why use Julia inside Nim ?

  • Extending Nim ecosystem with Julia Scientific package
  • As an efficient scripting language in a compiled application.

Tutorial

Nimjl already has some examples that explains the basics, make sure to go through them in order.

Basic stuff

Nimjl is mostly a Nim wrapper around the C API of Julia; and then some syntax sugar around it to make it easier to use. That means that inherently, Nimjl is limited to the capabilities of the C-API of Julia.

Now let's see what the code looks like :

import nimjl
Julia.init() # Must be done once in the lifetime of your program

discard Julia.println("Hello world !") # Invoke the println function from Julia. This function return a nil JlValue

Julia.exit() # -> This call is optionnal since it's called at the end of the process but making the exit explicit makes code more readable.
# All successive Julia calls after the exit will probably segfault
Hello world !

The Julia.init() calls initialize the Julia VM. No call before the init will work.

The Julia.exit() calls is optional since it's added as an exit procs (see std/exitprocs )

Internally, this is rewritten to :

discard jlCall("println", "Hello world !")

Both codes are identical; like mentioned above the Julia. is syntactic sugar for calling Julia functions; it always returns a JlValue (that can be nil if the Julia function does not return anything).

The equivalent C code would be :

    jl_function_t *func = jl_get_function(jl_base_module, "println");
    jl_value_t *argument = jl_eval_string("Hello world !");
    jl_call1(func, argument);

As mentioned, Julia is dynamically typed, which means that from Nim's point of view, every Julia object is a pointer of the C struct jl_value_t - mapped in Nim to JlValue.

Converting Nim type to Julia value

Most Nim values can be converted to JlValue through the function toJlVal or its alias toJlValue (I always got the two name confused so I ended up defining both...).

When passing a Nim value as a Julia argument through jlCall or Julia.myfunction, Nim will automatically convert the argument by calling toJlVal.

Let's see in practice what it means:

import nimjl
Julia.init()
var res = Julia.sqrt(255.0)
echo res

echo typeof(res)
echo jltypeof(res)
15.968719422671311
JlValue
Float64

This operation will perform a copy (almost always).

For reference, the equivalent C code would be :

jl_function_t *func = jl_get_function(jl_base_module, "sqrt");
jl_value_t *argument = jl_box_float64(255.0);
jl_value_t *ret = jl_call1(func, argument);
double cret = jl_unbox_float64(ret);
printf("cret=%f \n", cret);

Converting from Julia to Nim

In the previous example we calculated the square root of 255.0, stored in a JlValue. But using JlValue in Nim is hardly practical, so let's how to convert it back to a float:

var nimRes = res.to(float64)
echo nimRes
echo typeof(nimRes)
import std/math
# Check the result
assert nimRes == sqrt(255.0)
15.96871942267131
float64

For convenience:

  • proc jltypeof(x: JlVal) : string will invoke the Julia function typeof and convert the result to a string.
  • proc `$`(x: JlVal) : string will call the Julia function string and convert the result to a string - this allow us to call echo with JlValue and obtain the same output as Julia's println.

Keep these procs in mind as they will often be used in the following examples.