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 segfaultHello 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) : stringwill invoke the Julia function- typeofand convert the result to a string.
- proc `$`(x: JlVal) : stringwill call the Julia function- stringand convert the result to a string - this allow us to call- echowith 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.