Packages
mob_dev
0.5.17
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mob_dev/static_nifs.ex
defmodule MobDev.StaticNifs do
@moduledoc """
Schema, defaults, and C-source generation for the static NIF table.
The static NIF table lives in two C files inside an app's project:
priv/generated/driver_tab_ios.c
priv/generated/driver_tab_android.c
Both are linked **before** `libbeam.a` so they override BEAM's empty
built-in `erts_static_nif_tab[]`. With these in place, `load_nif/2`
resolves to the in-binary init function instead of falling back to
`dlopen`, which fails on iOS (App Store rejects bundled `.dylibs`) and
on Android (RTLD_LOCAL hides the parent's `enif_*` symbols from
child libraries).
## Declaring NIFs
An app's `mob.exs` may add to or override the defaults via the
`:static_nifs` key:
config :mob_dev,
static_nifs: [
%{module: :my_native, archs: [:all]}
]
Each entry is a map with these fields:
| Field | Type | Default | Meaning |
|------------|----------|-----------|----------------------------------------|
| `:module` | atom | required | Erlang module name |
| `:init` | string | derived | Init fn name. Defaults to `<mod>_nif_init` |
| `:builtin` | boolean | `false` | True for OTP-shipped libs |
| `:archs` | [atom] | `[:all]` | Where this NIF should appear |
| `:guard` | string | none | Preprocessor macro that gates the entry |
Valid `:archs` values: `:all`, `:ios`, `:android`, `:ios_sim`,
`:ios_device`, `:android_arm64`, `:android_arm32`.
When `:archs` is a strict subset of a target platform's archs (e.g.
`[:ios_device]` for iOS), set `:guard` to a preprocessor macro that the
build defines only on those archs. The generated C file wraps both the
forward declaration and the table row in `#ifdef <guard>`.
## Defaults
See `default_nifs/0` for the baked-in NIF set. It mirrors the hand-edited
`driver_tab_ios.c` / `driver_tab_android.c` files mob shipped through
v0.5.18 — `regen/1` against an empty user list produces byte-equivalent
output to those files.
"""
# Top-level platforms cover both arches; per-arch "platforms" are
# accepted by `on_platform?/2` so cross-compile callers (e.g.
# `NativeBuild.project_nif_zig_args/1`) can filter entries against a
# specific ABI.
@type platform :: :ios | :android | arch()
@type arch ::
:all
| :ios
| :android
| :ios_sim
| :ios_device
| :android_arm64
| :android_arm32
@type nif_entry :: %{
required(:module) => atom(),
optional(:init) => String.t(),
optional(:builtin) => boolean(),
optional(:archs) => [arch()],
optional(:guard) => String.t()
}
@valid_archs [
:all,
:ios,
:android,
:ios_sim,
:ios_device,
:android_arm64,
:android_arm32
]
@doc """
Returns the baked-in NIF set used by every Mob app.
These match the hand-edited `driver_tab_*.c` files in mob ≤ 0.5.18.
Users append to this list via `:static_nifs` in `mob.exs`.
"""
@spec default_nifs() :: [nif_entry()]
def default_nifs do
[
%{module: :prim_tty},
%{module: :erl_tracer},
%{module: :prim_buffer},
%{module: :prim_file},
%{module: :zlib},
%{module: :zstd},
%{module: :prim_socket},
%{module: :prim_net},
%{module: :asn1rt_nif, builtin: true},
%{module: :crypto, builtin: true},
%{module: :mob_nif},
%{
module: :sqlite3_nif,
archs: [:ios_device],
guard: "MOB_STATIC_SQLITE_NIF"
},
# EMLX NIF — statically linked when the project enables MLX via
# `mix mob.enable mlx`. The guard means the entry only fires when
# the build defines MOB_STATIC_EMLX_NIF, so apps that don't use Nx
# pay zero size cost. See MobDev.MLXDownloader for tarball fetching.
%{
module: :emlx_nif,
archs: [:ios_device, :ios_sim],
guard: "MOB_STATIC_EMLX_NIF"
},
# NxEigen NIF (Eigen-backed Nx backend) — statically linked when the
# project enables it via `mix mob.enable nxeigen`. Available on iOS
# AND Android (Eigen is header-only C++; mob_dev cross-compiles
# libnx_eigen.a per arch). Guard `MOB_STATIC_NX_EIGEN_NIF` keeps the
# entry zero-cost for apps that don't use Nx. See MobDev.NxEigenNif
# for the cross-compile.
%{
module: :nx_eigen,
archs: [:all],
guard: "MOB_STATIC_NX_EIGEN_NIF"
},
# TFLite NIF (TensorFlow Lite Nx backend) — statically linked when
# the project enables it via `mix mob.enable tflite`. Cross-platform:
# NNAPI on Android (vendor GPU/NPU HAL), CoreML on iOS (Apple
# Neural Engine). The TFLite runtime itself
# (`libtensorflowlite_jni.so` on Android,
# `TensorFlowLiteC.framework` on iOS) ships as a separate dynamic
# library bundled with the .apk / .app — only the NIF init has to be
# static. See `MobDev.TfliteNif` for the per-arch cross-compile and
# `MobDev.TfliteDownloader` for the runtime fetch/cache.
%{
module: :tflite_nif,
archs: [:all],
guard: "MOB_STATIC_TFLITE_NIF"
}
]
end
@doc """
Combines the defaults with a user list (typically from
`Application.get_env(:mob_dev, :static_nifs, [])`).
Later entries with the same `:module` override earlier ones. This lets
users replace a default entry — e.g. drop `:sqlite3_nif` by setting
`archs: []` — without forking the default list.
"""
@spec resolve(user_nifs :: [nif_entry()]) :: [nif_entry()]
def resolve(user_nifs) when is_list(user_nifs) do
(default_nifs() ++ user_nifs)
|> Enum.reverse()
|> Enum.uniq_by(& &1.module)
|> Enum.reverse()
|> Enum.reject(&(Map.get(&1, :archs, [:all]) == []))
end
@doc """
Validates a single entry, returning `:ok` or `{:error, reason}`.
"""
@spec validate_entry(nif_entry()) :: :ok | {:error, String.t()}
def validate_entry(%{module: module} = entry) when is_atom(module) do
cond do
Map.has_key?(entry, :init) and not is_binary(entry.init) ->
{:error, ":init must be a string, got #{inspect(entry.init)}"}
Map.has_key?(entry, :builtin) and not is_boolean(entry.builtin) ->
{:error, ":builtin must be a boolean, got #{inspect(entry.builtin)}"}
Map.has_key?(entry, :guard) and not is_binary(entry.guard) ->
{:error, ":guard must be a string, got #{inspect(entry.guard)}"}
true ->
validate_archs(Map.get(entry, :archs, [:all]))
end
end
def validate_entry(other), do: {:error, "expected a map with :module, got #{inspect(other)}"}
defp validate_archs(archs) when is_list(archs) do
case Enum.reject(archs, &(&1 in @valid_archs)) do
[] -> :ok
bad -> {:error, "unknown archs: #{inspect(bad)}; valid: #{inspect(@valid_archs)}"}
end
end
defp validate_archs(other), do: {:error, ":archs must be a list, got #{inspect(other)}"}
@doc """
Returns the init function name for an entry — either the explicit `:init`
value or the conventional `<module>_nif_init`.
"""
@spec init_fn(nif_entry()) :: String.t()
def init_fn(%{init: init}) when is_binary(init), do: init
def init_fn(%{module: module}), do: "#{module}_nif_init"
@doc """
Returns true if the entry should appear in the generated file for this
platform (i.e. its archs intersect the platform's archs).
"""
@spec on_platform?(nif_entry(), platform()) :: boolean()
def on_platform?(entry, platform) do
entry_archs = Map.get(entry, :archs, [:all]) |> expand_archs() |> MapSet.new()
platform_archs = platform_archs(platform) |> MapSet.new()
not MapSet.disjoint?(entry_archs, platform_archs)
end
@doc """
Returns true if the entry's archs are a *strict* subset of the platform's
archs (i.e. it's present on this platform but not all of its arches). When
true, the generated entry must be wrapped in `#ifdef <guard>`.
"""
@spec needs_guard?(nif_entry(), platform()) :: boolean()
def needs_guard?(entry, platform) do
entry_archs = Map.get(entry, :archs, [:all]) |> expand_archs() |> MapSet.new()
platform_archs = platform_archs(platform) |> MapSet.new()
on_platform?(entry, platform) and not MapSet.subset?(platform_archs, entry_archs)
end
defp platform_archs(:ios), do: [:ios_sim, :ios_device]
defp platform_archs(:android), do: [:android_arm64, :android_arm32]
# Per-arch "platforms" — used by `NativeBuild.project_nif_zig_args/1`
# to filter user NIF entries against a specific ABI when the iOS or
# Android build path needs to cross-compile per-ABI (e.g. Android's
# arm64 + armv7 .sos each get their own static-NIF set). Each is a
# singleton list so `on_platform?/2`'s intersection check still
# behaves the way the broader `:ios`/`:android` callers expect.
defp platform_archs(:ios_device), do: [:ios_device]
defp platform_archs(:ios_sim), do: [:ios_sim]
defp platform_archs(:android_arm64), do: [:android_arm64]
defp platform_archs(:android_arm32), do: [:android_arm32]
defp expand_archs(archs) do
Enum.flat_map(archs, fn
:all -> [:ios_sim, :ios_device, :android_arm64, :android_arm32]
:ios -> [:ios_sim, :ios_device]
:android -> [:android_arm64, :android_arm32]
other -> [other]
end)
end
@doc """
Generates the driver_tab source for one platform.
Format is `:c` by default (produces `driver_tab_<platform>.c` matching
the hand-edited reference files byte-for-byte). Pass `format: :zig`
for the Phase 6a Zig output — same semantics, structured as
comptime-friendly Zig (`extern struct` ABI types, `export` for the
C-callable symbols, `if (sqlite_static) ... else ...` in place of
`#ifdef`).
Pure function — given the same nif list it always produces the same
bytes.
"""
@spec generate(platform(), [nif_entry()]) :: iodata()
@spec generate(platform(), [nif_entry()], keyword()) :: iodata()
def generate(platform, nifs, opts \\ []) when platform in [:ios, :android] do
case Keyword.get(opts, :format, :c) do
:c -> generate_c(platform, nifs)
:zig -> generate_zig(platform, nifs)
end
end
defp generate_c(platform, nifs) do
applicable = Enum.filter(nifs, &on_platform?(&1, platform))
[
header(platform),
driver_tab_block(),
forward_decls(applicable, platform),
"\n",
static_nif_tab(applicable, platform)
]
end
defp generate_zig(platform, nifs) do
applicable = Enum.filter(nifs, &on_platform?(&1, platform))
[
zig_header(platform),
zig_extern_decls(applicable, platform),
"\n",
zig_driver_tab_block(),
zig_static_nif_tab(applicable, platform)
]
end
# ── Zig output ────────────────────────────────────────────────────────────
defp zig_header(:ios) do
"""
//! driver_tab_ios.zig — Static NIF table generated by mix mob.regen_driver_tab.
//! DO NOT EDIT. Regenerate via `mix mob.regen_driver_tab` after changing
//! :static_nifs in mob.exs.
//!
//! Linked BEFORE libbeam.a so it overrides BEAM's empty built-in driver_tab.
const ErtsStaticDriver = extern struct {
de: ?*anyopaque,
flags: c_int,
};
const ErtsStaticNif = extern struct {
nif_init: ?*const fn () callconv(.c) ?*anyopaque,
is_builtin: c_int,
nif_mod: c_ulong,
entry: ?*anyopaque,
};
const ErlDrvEntryStub = extern struct {
de: ?*anyopaque,
flags: c_int,
};
const THE_NON_VALUE: c_ulong = 0;
extern var inet_driver_entry: ErlDrvEntryStub;
extern var ram_file_driver_entry: ErlDrvEntryStub;
"""
end
defp zig_header(:android) do
"""
//! driver_tab_android.zig — Static NIF table generated by mix mob.regen_driver_tab.
//! DO NOT EDIT. Regenerate via `mix mob.regen_driver_tab` after changing
//! :static_nifs in mob.exs.
//!
//! Linked BEFORE libbeam.a so it overrides BEAM's empty built-in driver_tab.
const ErtsStaticDriver = extern struct {
de: ?*anyopaque,
flags: c_int,
};
const ErtsStaticNif = extern struct {
nif_init: ?*const fn () callconv(.c) ?*anyopaque,
is_builtin: c_int,
nif_mod: c_ulong,
entry: ?*anyopaque,
};
const ErlDrvEntryStub = extern struct {
de: ?*anyopaque,
flags: c_int,
};
const THE_NON_VALUE: c_ulong = 0;
extern var inet_driver_entry: ErlDrvEntryStub;
extern var ram_file_driver_entry: ErlDrvEntryStub;
"""
end
defp zig_extern_decls(nifs, platform) do
# iOS: each guarded NIF (e.g. :sqlite3_nif on device only, :emlx_nif
# when EMLX is enabled) gets its own comptime flag, threaded in via
# `b.addOptions` in build_device.zig. Flag name is derived from the
# `:guard` value — see guard_flag_name/1.
plain =
nifs
|> Enum.reject(&needs_guard_in_zig?(&1, platform))
|> Enum.map(fn nif ->
"extern fn #{init_fn(nif)}() callconv(.c) ?*anyopaque;\n"
end)
guarded = Enum.filter(nifs, &needs_guard_in_zig?(&1, platform))
guard_imports =
case guarded do
[] ->
[]
_ ->
flag_decls =
guarded
|> Enum.map(& &1.guard)
|> Enum.uniq()
|> Enum.map(fn guard ->
"const #{guard_flag_name(guard)} = build_options.#{guard_flag_name(guard)};\n"
end)
[
"\n",
"// Comptime flags threaded from build.zig via b.addOptions().\n",
"// Each per-feature flag defaults to false; the build sets it to true\n",
"// when the project opts into the corresponding statically-linked NIF.\n",
"const build_options = @import(\"build_options\");\n",
flag_decls,
"\n",
Enum.map(guarded, fn nif ->
"extern fn #{init_fn(nif)}() callconv(.c) ?*anyopaque;\n"
end)
]
end
[plain, guard_imports]
end
defp needs_guard_in_zig?(nif, platform) do
guarded?(nif) and on_platform?(nif, platform)
end
@doc """
True when the entry carries a `:guard` key. The guard is the user's
explicit opt-in (e.g. `MOB_STATIC_EMLX_NIF`) and gets emitted as a
preprocessor `#ifdef` in C output or a comptime const in Zig output,
independent of whether the entry's archs narrow the platform.
"""
@spec guarded?(nif_entry()) :: boolean()
def guarded?(nif), do: Map.has_key?(nif, :guard)
# Convention: MOB_STATIC_SQLITE_NIF → sqlite_static, MOB_STATIC_EMLX_NIF →
# emlx_static. Lowercased, stripped of the `MOB_STATIC_` prefix and the
# `_NIF` suffix. Future guards follow the same convention.
defp guard_flag_name(guard) when is_binary(guard) do
guard
|> String.replace_prefix("MOB_STATIC_", "")
|> String.replace_suffix("_NIF", "")
|> String.downcase()
|> Kernel.<>("_static")
end
defp zig_driver_tab_block do
"""
export var driver_tab: [3]ErtsStaticDriver = .{
.{ .de = &inet_driver_entry, .flags = 0 },
.{ .de = &ram_file_driver_entry, .flags = 0 },
.{ .de = null, .flags = 0 },
};
export fn erts_init_static_drivers() callconv(.c) void {}
"""
end
defp zig_static_nif_tab(nifs, platform) do
plain_nifs = Enum.reject(nifs, &needs_guard_in_zig?(&1, platform))
guarded_nifs = Enum.filter(nifs, &needs_guard_in_zig?(&1, platform))
base_rows = Enum.map(plain_nifs, &zig_nif_row/1)
sentinel_row =
" .{ .nif_init = null, .is_builtin = 0, .nif_mod = THE_NON_VALUE, .entry = null },\n"
case guarded_nifs do
[] ->
[
"export var erts_static_nif_tab = [_]ErtsStaticNif{\n",
base_rows,
sentinel_row,
"};\n"
]
[_ | _] ->
# Build the table comptime — base rows + per-flag conditionally-added
# guarded rows + sentinel. Each guarded NIF gets its own ErtsStaticNif
# const, and the final array is selected via a branching block.
per_nif_consts =
Enum.map(guarded_nifs, fn nif ->
init = init_fn(nif)
builtin = if Map.get(nif, :builtin, false), do: "1", else: "0"
"const #{nif_const_name(nif)} = ErtsStaticNif{ " <>
".nif_init = #{init}, .is_builtin = #{builtin}, " <>
".nif_mod = THE_NON_VALUE, .entry = null };\n"
end)
[
"const base_nifs = [_]ErtsStaticNif{\n",
base_rows,
"};\n\n",
per_nif_consts,
"\n",
"const sentinel = ErtsStaticNif{ .nif_init = null, .is_builtin = 0, .nif_mod = THE_NON_VALUE, .entry = null };\n\n",
zig_branching_table(guarded_nifs)
]
end
end
# Builds the `export var erts_static_nif_tab = blk: { ... }` chain.
# For N guarded NIFs we emit 2^N branches enumerating every subset of
# active guards. N=1 → 2 branches (current sqlite-only behavior). N=2 →
# 4 branches (sqlite + emlx). Higher N is theoretical for now.
defp zig_branching_table(guarded_nifs) do
n = length(guarded_nifs)
subsets = subsets(guarded_nifs)
branches =
subsets
# Most-specific subsets first (all flags true) so `if (a and b)` shadows
# `if (a)` correctly.
|> Enum.sort_by(&(-length(&1)))
|> Enum.with_index()
|> Enum.map(fn {active, idx} ->
condition = zig_branch_condition(active, guarded_nifs)
rows_expr = zig_branch_rows(active)
keyword = if idx == 0, do: "if", else: "} else if"
if length(active) == 0 do
"} else {\n break :blk #{rows_expr};\n"
else
"#{keyword} (#{condition}) {\n break :blk #{rows_expr};\n"
end
end)
case n do
1 ->
# Simpler shape for the common N=1 case — matches the legacy output.
[nif] = guarded_nifs
[
"export var erts_static_nif_tab = blk: {\n",
" if (#{guard_flag_name(nif.guard)}) {\n",
" break :blk base_nifs ++ [_]ErtsStaticNif{ #{nif_const_name(nif)}, sentinel };\n",
" } else {\n",
" break :blk base_nifs ++ [_]ErtsStaticNif{sentinel};\n",
" }\n",
"};\n"
]
_ ->
[
"export var erts_static_nif_tab = blk: {\n ",
Enum.intersperse(branches, " "),
" }\n};\n"
]
end
end
defp subsets([]), do: [[]]
defp subsets([h | t]) do
rest = subsets(t)
rest ++ Enum.map(rest, &[h | &1])
end
defp zig_branch_condition([], _all), do: "true"
defp zig_branch_condition(active, _all) do
active
|> Enum.map(&guard_flag_name(&1.guard))
|> Enum.join(" and ")
end
defp zig_branch_rows([]) do
"base_nifs ++ [_]ErtsStaticNif{sentinel}"
end
defp zig_branch_rows(active) do
extras =
active
|> Enum.map(&nif_const_name/1)
|> Enum.join(", ")
"base_nifs ++ [_]ErtsStaticNif{ #{extras}, sentinel }"
end
defp nif_const_name(%{module: module}), do: "#{module}_const"
defp zig_nif_row(nif) do
init = init_fn(nif)
builtin = if Map.get(nif, :builtin, false), do: "1", else: "0"
" .{ .nif_init = #{init}, .is_builtin = #{builtin}, .nif_mod = THE_NON_VALUE, .entry = null },\n"
end
defp header(:ios) do
"""
// driver_tab_ios.c — Static NIF table generated by mix mob.regen_driver_tab.
// DO NOT EDIT. Regenerate via `mix mob.regen_driver_tab` after changing
// :static_nifs in mob.exs.
//
// Linked BEFORE libbeam.a so it overrides BEAM's empty built-in driver_tab.
#include <stddef.h>
typedef struct { void* de; int flags; } ErtsStaticDriver;
#define THE_NON_VALUE ((unsigned long)0)
typedef struct {
void* (*nif_init)(void);
int is_builtin;
unsigned long nif_mod;
void* entry;
} ErtsStaticNif;
typedef struct { void* de; int flags; } ErlDrvEntryStub;
extern ErlDrvEntryStub inet_driver_entry;
extern ErlDrvEntryStub ram_file_driver_entry;
"""
end
defp header(:android) do
"""
// driver_tab_android.c — Static NIF table generated by mix mob.regen_driver_tab.
// DO NOT EDIT. Regenerate via `mix mob.regen_driver_tab` after changing
// :static_nifs in mob.exs.
//
// Linked BEFORE libbeam.a so it overrides BEAM's empty built-in driver_tab.
#include <stddef.h>
typedef struct { void* de; int flags; } ErtsStaticDriver;
#define THE_NON_VALUE ((unsigned long)0)
typedef struct {
void* (*nif_init)(void);
int is_builtin;
unsigned long nif_mod;
void* entry;
} ErtsStaticNif;
typedef struct { void* de; int flags; } ErlDrvEntryStub;
extern ErlDrvEntryStub inet_driver_entry;
extern ErlDrvEntryStub ram_file_driver_entry;
"""
end
defp driver_tab_block do
"""
ErtsStaticDriver driver_tab[] = {
{&inet_driver_entry, 0},
{&ram_file_driver_entry, 0},
{NULL, 0}
};
void erts_init_static_drivers(void) {}
"""
end
defp forward_decls(nifs, platform) do
Enum.map(nifs, fn nif ->
decl = "void *#{init_fn(nif)}(void);\n"
if guarded?(nif) and on_platform?(nif, platform) do
"#ifdef #{nif.guard}\n#{decl}#endif\n"
else
decl
end
end)
end
defp static_nif_tab(nifs, platform) do
rows =
Enum.map(nifs, fn nif ->
row = format_row(nif)
if guarded?(nif) and on_platform?(nif, platform) do
"#ifdef #{nif.guard}\n #{row}\n#endif\n"
else
" #{row}\n"
end
end)
[
"ErtsStaticNif erts_static_nif_tab[] = {\n",
rows,
" {NULL, 0, THE_NON_VALUE, NULL}\n};\n"
]
end
defp format_row(nif) do
init = init_fn(nif)
builtin = if Map.get(nif, :builtin, false), do: "1", else: "0"
# Pad the init name to 22 chars so columns align like the hand-edited file.
padded = String.pad_trailing("#{init},", 23)
"{#{padded}#{builtin}, THE_NON_VALUE, NULL},"
end
end