How to call Swift 5.3 code from Elixir via NIF

The Mac series with the Apple Silicon M1 chip has been released. Weren’t you surprised by the amazing performance of it? I was also. I hoped we can use the wonderful power and Apple’s ecosystem for Elixir programming, so I tried to write Elixir code to call Swift code. This article shows how to call Swift code from Elixir via NIF.

Here is the repository of such a sample code:

Nov 24, 2020: I updated "Makefile" in the repository in order to be able to build it in case of initialized Mac, so the bellow figures includes some wrong points, though the explanations are updated.

I’ll explain the details of it. Here we go!

How to call native code from Elixir code

First of all, I’ll explain how to call native code from Elixir code. You can use NIF for it. All you have to do is the following steps:

  1. Modify “mix.exs” to use “elixir_make”;
  2. Write C code;
  3. Write “Makefile” to compile it;
  4. Write Elixir code to load the library of native code and to call it

Use “elixir_make”

“elixir_make” is a hex module that enables us to build a project by the “make” command.

Modify mix.exs as follows:

Modification of mix.exs to use elixir_make

Then, run “mix deps.get” to import elixir_make.

Write C code

Run “mkdir -p c_src”, create a new file named “libnif.c”, and write the following temporary C code on the c_src directory:

Description of the temporary C code

“erl_nif.h” is the header file of NIF of Erlang. It is required when you use the NIF API.

The function “test” in C code has three arguments, “env”, “argc” and “argv”. “env” stands for the environment on Erlang VM. “argc” and “argv” form a variable length array of the arguments of “test” in Elixir code.

This function returns the tuple of atoms {:error, :not_implemented}, temporally. “enif_make_atom” creates an atom with the value given by the 2nd argument of it using the environment on Erlang VM given by the 1st argument of it. “enif_make_tuple” creates a tuple that contains some elements using the environment given by the 1st argument of it. It has variable length arguments. The number of the elements is given by the 2nd argument of it. The bodies of the elements are given by the 3rd and subsequent arguments.

“nif_funcs” is a function table of NIF function. It gives the information that there is a NIF function named as “test”. The number 0 in “nif_funcs” stands for the arity of the function “test”, which means “test” in Elixir code has no arguments.

“ERL_NIF_INIT” gives the entry point of NIF functions. In this case, “SwiftNifTest” should be a stub Elixir module that has all of the functions defined in “nif_func”, which is defined in the “lib” directory. If not, you’ll get an error when the module is loaded. You can use another module, but you should define it in the “lib” directory. You can also describe reloading behavior in “ERL_NIF_INIT”, but it will be omitted in this article.

Write “Makefile”

Create a new file named “Makefile” in the top directory of the Elixir module, and write the following code:

Makefile

I’ll explain the summary of it:

  1. The native compiler should be Apple Clang, because it’ll compile Objective-C and Swift code, so “CC” should be defined as “xcrun clang” when compiling on Mac.
  2. The environment variables “ERL_EI_INCLUDE_DIR” and “ERL_EI_LIBDIR” will be set to the directory of include headers and libraries of Erlang, respectively, in case of cross-compiling by Nerves. If not, they should be set correctly by this “Makefile”.
  3. The environment variable “LDFLAGS” should be set “-shared” option, at least. In addition, when not cross-compiling nor Windows, the environment variables “LDFLAGS” and “CFLAGS” should be set “-fPIC” option. Furthermore, in case of Mac, “LDFLAGS” should be set “-dynamiclib” and “-undefined dynamic_lookup” options.
  4. The target that is compiled and linked by this “Makefile” is located at “priv/libnif.so”, which is specified by “NIF”.
  5. The source files are specified by “C_SRCS”. The object files are automatically defined as “C_OBJS” by replacing substrings of “C_SRCS”. They are generated in the directory “obj”.
  6. This “Makefile” also identifies dependencies of the source code, automatically. This is realized by “C_DEPS”, compiling each of the source code with “-MM -MP -MF” options, and include the dependencies.

Write the stub Elixir code

Modify Elixir code in the “lib” directory to load the NIF library and form a stub to call the NIF functions defined in the library:

“@on_load” with an atom forces the module to execute the entry function whose name is given by the atom when the module is loaded.

“:code.priv_dir(:swift_nif_test)” gives the “priv” directory in the top directory of the module “SwiftNifTest”.

“:erlang.load_nif” makes the module load the NIF library located at the path specified by the 1st argument (“nif_file”), which returns “:ok” when it succeeded, or “:error” with some reason.

This module should define the stub of each of the NIF functions. The “test” function is so. It should be defined as code raising an exception, which is executed when it is called in case that the NIF library wasn’t loaded.

Test NIF code

Run “iex -S mix”, run “SwiftNifTest.test()” on the iex, and you’ll get the result “{:error, :not_implemented}”, which is defined the “test” function in “libnif.c”, temporally.

How to call Swift 5.3 code from Objective-C code

It is impossible to call Swift code from C code, directly. Swift code should be called from C code via Objective-C code.

Callee Swift Code

Example Swift code is here:

Example Swift code

The original code of it is from here:

To call it from Objective-C, you add “@objc” to the definitions of the class and the functions that you want to call from Objective-C.

Caller Objective-C code

To call the Swift code from C code, you should define a wrapper C function in Objective-C code that calls the Swift code, which is composed of the following header and module files in the “c_src” directory:

caller.h
caller.m

The results that equals that by the following Swift code will be expected to be shown by calling the function “caller”:

“ExampleClass-Swift.h” is the most important point to compile and link them. Of course, “ExampleClass” is corresponding to the name of the class that is defined in the Swift code. It should be replaced into another name when the class will be renamed. The Swift compiler “swiftc” with “-emit-objc-header” option can generate it from the Swift code (I’ll explain it, later).

Modification of “Makefile”

To compile and link them, Modify “Makefile” as follows:

The modification of Makefile in order to compile and link Swift and Objective-C code

The summary of the modification of “Makefile” is as follows:

  1. Specify the library path to be made include Swift libraries by setting
    “LDFLAGS += -L`xcrun — show-sdk-path`/usr/lib/swift”;
  2. Compile the Objective-C code by Apple Clang ("xcrun clang");
  3. Compile the Swift code by “swiftc” ("xcrun swiftc") with the options “-emit-object -parse-as-library”;
  4. Generate the header “ExampleClass-Swift.h” by “swiftc” (“xcrun swiftc”) with the options “-emit-objc-header -emit-objc-header-path”;
  5. Link them and C code by “Apple Clang” (“xcrun clang”) with the library path specified by 1.

The results

Run “iex -S mix”, “SwiftNifTest.test()” and get the following results:

The execution results

That’s all!

Call me ZACKY. I'm a researcher of Elixir. My works are including Pelemay https://github.com/zeam-vm/pelemay/, (its old name is Hastega) .