How do you package a lib with a NIF?

I’m creating a NIF, and having some issues around packaging.

  • I started with an empty Elixir app, added the NIF source in its own directory, the appropriate task to mix.exs to build shared library into priv, and the wrapper module with @on_load of :erlang.load_nif('priv/foo.so', nil)

  • That worked: I could iex -S mix run --no-halt then exercise the NIF using Foo.func(...); also foo’s tests worked.

  • I want to package this as a library that can just be pulled in as a normal dependency. It’s just simple functions, no need for its own application or any processes. So I deleted lib/foo_app.ex and lib/foo_app/application.ex, and in mix.exs removed the mod: entry from the application. mix compile and mix test both work.

  • Now for the problems: using it. I create a new empty application, add the dependency {:foo, path: "../foo", app: false}. Compile works, but when I try to run (or test), the load of the NIF fails, because it’s not in priv relative to the executing project location, but rather in _build/dev/lib/local_dependency

  • It seems that maybe to load the NIF I should use Application.app_dir(:foo, "priv") but that gives an error about unknown application, while :code.priv_dir(:foo) gives :bad_name. Also, I note that mix release does not copy the NIF lib at all, not even if I add Mix.Project.build_structure to the end of the compile mix task after priv/foo.so is created.

  • So maybe I have to have an empty dummy application in the lib project, just to make all the build parts happy? And indeed, adding back a dummy app, setting the mod: to point to it, etc, makes things work. But of course the dummy app needs a start/2 with a return of the right form, so:

defmodule Foo.App do
  use Application
  def start(_, _) do
    Supervisor.start_link([], [strategy: :one_for_one])
  end
end

It seems silly to have to implement a do-nothing supervisor in order for the NIF lib to be copied and able to be located. Isn’t there a better way to structure and pull in this lib???

Not a supervisor, but a no-op Application is all you need, it doesn’t need to start anything at all. Priv directories are in relation to an Application root (as are config files and more too). :slight_smile:

4 Likes

OK, I see now:

  • Leave out mod: ... in the application def in mix.exs
  • But do have a module which matches the app: ... name
  • But leave the module totally empty, no use Application, no start/2
1 Like

Well use Application, as that is what defines the entrance, but the start is generally just a nop.

When I tried start as a noop, it complained about the return value. (I did not try just returning {:ok, nil})

When I remove both the start function and use Application, it still works. Apparently the use is not needed, just a module whose name matches the app: key in the project def is enough for it to base paths on that module.

Also, instead of function application returning [], I just removed that function altogether from mix.exs, and it still works.

1 Like

Actually I ‘think’ you can just leave the start function out entirely from the callback module, at least you could in erlang if I recall right. Otherwise an empty supervisor pid is fine.

I didn’t realize that, and didn’t try it–just assumed something would complain about the bogus pid.

The key to leaving out start in the module is leaving out mod: ... in mix.exs–that results in it not trying to call start, and also seems to allow for dropping use Application

1 Like

Yep, you can even see part of that in mix help new:

A --sup option can be given to generate an OTP application skeleton including a
supervision tree. Normally an app is generated without a supervisor and without
the app callback.