MobDev.NxEigenNif (mob_dev v0.5.7)

Copy Markdown View Source

Cross-compiles the nx_eigen C++ NIF (Eigen-backed Nx backend) for one Android or iOS target ABI and archives the result as libnx_eigen.a. The archive gets static-linked into the user app's main native binary alongside crypto.a, libemlx.a, and any other static NIFs.

Same constraints as every other NIF mob ships on phones:

  • Android. dlopen'd children inherit RTLD_LOCAL, hiding the parent's enif_* symbols from a separately-loaded libnx_eigen.so. on_load then fails with "cannot locate symbol". Static linking sidesteps that — BEAM finds nx_eigen_nif_init via dlsym(RTLD_DEFAULT) against the main app binary.
  • iOS. App Store forbids loading unsigned dylibs / dlopen; every NIF must already be present in the signed binary.

Per-target deltas

All four targets compile the same two source files (@sources) with the shared base CXXFLAGS list (@base_cxxflags). The deltas are:

TargetArch dirToolchainExtra CXXFLAGSnm symbol
android_arm64aarch64-unknown-linux-androidNDK clang++/llvm-arAndroid hardening: branch-protect, stack-clash, _GNU_SOURCEnx_eigen_nif_init
android_arm32arm-unknown-linux-androideabiNDK clang++/llvm-arAndroid hardening + -march=armv7-a -mfloat-abi=softfp -mthumbnx_eigen_nif_init
ios_simaarch64-apple-iossimulatorxcrun (sim SDK)iOS minimal — no Android hardening_nx_eigen_nif_init
ios_deviceaarch64-apple-iosxcrun (device SDK)iOS minimal — no Android hardening_nx_eigen_nif_init

STATIC_ERLANG_NIF_LIBNAME

We pass -DSTATIC_ERLANG_NIF_LIBNAME=nx_eigen rather than plain -DSTATIC_ERLANG_NIF. The reason: nx_eigen_nif.cpp uses Fine's FINE_INIT(...) macro, which expands to ERL_NIF_INIT_DECL(NAME) — passing the literal token NAME as MODNAME. With STATIC_ERLANG_NIF alone the emitted symbol would be the unhelpful NAME_nif_init. Setting LIBNAME overrides MODNAME entirely and forces the symbol to nx_eigen_nif_init, which is what mob's driver_tab references.

FFT — Eigen's built-in kissfft backend

We don't use NxEigen's bundled FFT variants. Instead, mob_dev ships its own priv/cpp_nif/nx_eigen_fft_eigen.cpp bridge that calls Eigen's unsupported/Eigen/FFT module (kissfft underneath — header only, embedded in the Eigen tarball). This gives Nx.fft/3 and Nx.ifft/3 working on-device with no additional cross-compile.

Kissfft is roughly 2x slower than FFTW for large transforms but microseconds for audio-sized buffers; switch to a FFTW variant later if a real workload measures a bottleneck.

Phases

Each build/2 call:

  1. Precheck — nx_eigen source dir + Fine + Eigen headers + erts include all present; Android/iOS toolchain reachable.
  2. Compile — for each of @sources, run <cxx> <cxxflags> -c -o obj src.
  3. Archive — <ar> rcs libnx_eigen.a obj1 obj2 then <ranlib> ....
  4. Verify — <nm> libnx_eigen.a, scan for the expected symbol. Symbol missing is a :precondition_failed (means our compile didn't actually produce nx_eigen_nif_init — usually because Fine or NxEigen's source moved out from under us, or LIBNAME wasn't picked up).

Summary

Functions

Base CXXFLAGS shared across all targets. Public for testing.

Compile + archive + verify libnx_eigen.a for one target. Returns {:ok, info} naming the produced archive, or a tagged error.

Parse nm output and confirm the expected nx_eigen_nif_init symbol is exported (T flag in nm's output). Returns :ok or a tagged precondition_failed.

Assemble the full CXXFLAGS list for a target plus the include path list. Pure function for testability — silent flag drops are the exact regression class this module exists to prevent.

Source files compiled for every target — list of {root, basename}. Public so tests can pin the surface.

Per-target spec. Public so tests can lock down the surface (especially the extra_cxxflags lists — silent drops there would silently weaken released binaries).

All known NxEigen targets.

Functions

base_cxxflags()

@spec base_cxxflags() :: [String.t()]

Base CXXFLAGS shared across all targets. Public for testing.

build(target_id, opts \\ [])

@spec build(
  atom(),
  keyword()
) :: {:ok, map()} | MobDev.Release.Errors.t()

Compile + archive + verify libnx_eigen.a for one target. Returns {:ok, info} naming the produced archive, or a tagged error.

Options:

  • :nx_eigen_dir — path to the nx_eigen Hex dep (the dir containing c_src/ and the Eigen download). Required.
  • :fine_dir — path to the fine Hex dep (containing c_include/). Required.
  • :erts_include — path to the per-target erts-VSN/include/ dir (carries erl_nif.h, etc.). Required.
  • :eigen_dir — Eigen header root (defaults to <nx_eigen_dir>/eigen-3.4.0).
  • :bridge_dir — directory holding the Eigen-FFT bridge source we ship (defaults to :code.priv_dir(:mob_dev)/cpp_nif).
  • :out_dir — directory the archive + per-arch obj subdir get written to. Required.
  • :ndk_root — Android NDK root (Android targets only; defaults to ~/Library/Android/sdk/ndk/<NdkVersion.effective()>).

check_symbol_present(nm_output, expected_symbol, archive)

@spec check_symbol_present(binary(), String.t(), Path.t()) ::
  :ok | MobDev.Release.Errors.t()

Parse nm output and confirm the expected nx_eigen_nif_init symbol is exported (T flag in nm's output). Returns :ok or a tagged precondition_failed.

Mirror of MobDev.Release.OpenSSL.CryptoNif.check_symbol_present/3 — the parsing logic is identical, just a different expected symbol. Missing symbol on an otherwise-successful build almost always means STATIC_ERLANG_NIF_LIBNAME=nx_eigen got dropped from the flags or Fine's FINE_INIT macro changed shape.

cxxflags(target, includes)

@spec cxxflags(MobDev.NxEigenNif.Target.t(), [Path.t()]) :: [String.t()]

Assemble the full CXXFLAGS list for a target plus the include path list. Pure function for testability — silent flag drops are the exact regression class this module exists to prevent.

includes is a list of absolute directory paths to be -I-prefixed. Order is preserved.

sources()

@spec sources() :: [{:nx_eigen | :bridge, String.t()}]

Source files compiled for every target — list of {root, basename}. Public so tests can pin the surface.

target_spec(atom)

@spec target_spec(atom()) :: MobDev.NxEigenNif.Target.t()

Per-target spec. Public so tests can lock down the surface (especially the extra_cxxflags lists — silent drops there would silently weaken released binaries).

targets()

@spec targets() :: [atom()]

All known NxEigen targets.