# `MobDev.NxEigenNif`
[🔗](https://github.com/genericjam/mob_dev/blob/master/lib/mob_dev/nx_eigen_nif.ex#L1)

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.

## Why static-link this NIF?

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:

| Target        | Arch dir                        | Toolchain          | Extra CXXFLAGS                                | nm symbol            |
|---------------|---------------------------------|--------------------|-----------------------------------------------|----------------------|
| android_arm64 | aarch64-unknown-linux-android   | NDK clang++/llvm-ar | Android hardening: branch-protect, stack-clash, _GNU_SOURCE | `nx_eigen_nif_init`   |
| android_arm32 | arm-unknown-linux-androideabi   | NDK clang++/llvm-ar | Android hardening + `-march=armv7-a -mfloat-abi=softfp -mthumb` | `nx_eigen_nif_init`   |
| ios_sim       | aarch64-apple-iossimulator      | xcrun (sim SDK)    | iOS minimal — no Android hardening            | `_nx_eigen_nif_init`  |
| ios_device    | aarch64-apple-ios               | xcrun (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).

# `base_cxxflags`

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

Base CXXFLAGS shared across all targets. Public for testing.

# `build`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

All known NxEigen targets.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
