Packages
mob_dev
0.6.0
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/tflite_nif.ex
defmodule MobDev.TfliteNif do
@moduledoc """
Cross-compiles `tflite_nif.c` (the NIF wrapping TensorFlow Lite's C API)
for one Android or iOS target ABI and archives the result as
`libtflite_nif.a`. The archive gets static-linked into the user app's
main native binary alongside `crypto.a`, `libemlx.a`, and any other
static NIFs.
## Companion bits
* **Native libs** — `MobDev.TfliteDownloader` fetches the TFLite native
libraries (AAR for Android, xcframework tarball for iOS) and supplies
headers + the runtime library that gets linked into the launcher
binary alongside this archive.
* **C source** — `tflite_nif.c` ships in the `:nx_tflite_mob` Hex dep
(the user adds `{:nx_tflite_mob, ...}` to their `mix.exs` when they
run `mix mob.enable tflite`). This module looks it up via
`:code.lib_dir(:nx_tflite_mob)`.
* **Static-NIF table entry** — `:tflite_nif` is registered in
`MobDev.StaticNifs.default_nifs/0` with guard `MOB_STATIC_TFLITE_NIF`.
The guard threads into `build_device.zig` as `tflite_static` and is
set to `true` when the project enables this feature.
## 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 `libtflite_nif.so`.
`on_load` then fails with "cannot locate symbol". Static linking
sidesteps that — BEAM finds `tflite_nif_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.
The TFLite runtime itself (`libtensorflowlite_jni.so` on Android,
`TensorFlowLiteC.framework` on iOS) IS allowed to load via the normal
dynamic-linker path: it's code-signed (iOS) or part of the standard
jniLibs/ contract (Android). Only the NIF init has to be static.
## Per-target deltas
| Target | Source | Compiler | nm symbol |
|---------------|---------------------|----------------------|----------------------|
| android_arm64 | NDK aarch64 clang | aarch64 cross | `tflite_nif_nif_init` |
| android_arm32 | NDK arm clang | arm cross (armv7-a) | `tflite_nif_nif_init` |
| ios_sim | xcrun iphonesimulator | arm64-apple-ios-simulator | `_tflite_nif_nif_init` |
| ios_device | xcrun iphoneos | arm64-apple-ios | `_tflite_nif_nif_init` |
## STATIC_ERLANG_NIF_LIBNAME
We pass `-DSTATIC_ERLANG_NIF_LIBNAME=tflite_nif` to make `ERL_NIF_INIT`
emit symbol `tflite_nif_nif_init`. That matches the convention used by
`MobDev.NxEigenNif` (`nx_eigen` → `nx_eigen_nif_init`) and what
`MobDev.StaticNifs.init_fn/1` derives for the `:tflite_nif` module
entry.
"""
alias MobDev.NdkVersion
alias MobDev.Release.{Errors, Shell}
@android_api 28
@ios_min_version "15.0"
@doc "All known TFLite NIF targets."
@spec targets() :: [atom()]
def targets, do: [:android_arm64, :android_arm32, :ios_sim, :ios_device]
# ── Target spec ─────────────────────────────────────────────────────────
defmodule Target do
@moduledoc false
@enforce_keys [:id, :tools_fn, :extra_cflags, :nm_symbol]
defstruct [:id, :tools_fn, :extra_cflags, :nm_symbol]
end
@android_extra_cflags [
"-fstrict-flex-arrays=3",
"-mbranch-protection=standard",
"-fstack-clash-protection",
"-D_GNU_SOURCE",
"-D__ANDROID__"
]
@arm32_extra_cflags ["-march=armv7-a", "-mfloat-abi=softfp", "-mthumb"]
@doc "Per-target spec. Public for testing."
@spec target_spec(atom()) :: %Target{}
def target_spec(:android_arm64) do
%Target{
id: :android_arm64,
tools_fn: &android_tools(&1, :android_arm64),
extra_cflags: @android_extra_cflags,
nm_symbol: "tflite_nif_nif_init"
}
end
def target_spec(:android_arm32) do
%Target{
id: :android_arm32,
tools_fn: &android_tools(&1, :android_arm32),
extra_cflags: @arm32_extra_cflags ++ @android_extra_cflags,
nm_symbol: "tflite_nif_nif_init"
}
end
def target_spec(:ios_sim) do
%Target{
id: :ios_sim,
tools_fn: &ios_tools(&1, :ios_sim),
extra_cflags: [],
nm_symbol: "_tflite_nif_nif_init"
}
end
def target_spec(:ios_device) do
%Target{
id: :ios_device,
tools_fn: &ios_tools(&1, :ios_device),
extra_cflags: [],
nm_symbol: "_tflite_nif_nif_init"
}
end
# ── CFLAGS assembly ─────────────────────────────────────────────────────
@base_cflags [
"-fPIC",
"-O2",
"-Wall",
"-std=c99",
"-DSTATIC_ERLANG_NIF_LIBNAME=tflite_nif"
]
@doc "Base CFLAGS shared across all targets. Public for testing."
@spec base_cflags() :: [String.t()]
def base_cflags, do: @base_cflags
@doc """
Assemble full CFLAGS for a target plus include / framework search paths.
`includes` are `-I`-prefixed; `frameworks` are `-F`-prefixed (iOS only —
ignored on Android targets). Order is preserved.
"""
@spec cflags(%Target{}, [Path.t()], [Path.t()]) :: [String.t()]
def cflags(%Target{} = target, includes, frameworks \\ [])
when is_list(includes) and is_list(frameworks) do
@base_cflags ++
target.extra_cflags ++
Enum.map(includes, &"-I#{&1}") ++
Enum.map(frameworks, &"-F#{&1}")
end
# ── Build entrypoint ────────────────────────────────────────────────────
@doc """
Compile + archive + verify `libtflite_nif.a` for one target.
Options:
* `:nx_tflite_mob_dir` — path to the `:nx_tflite_mob` Hex dep (the
dir containing `c_src/tflite_nif.c`). **Required.**
* `:tflite_dir` — path returned by `MobDev.TfliteDownloader.ensure/1`
for this target. **Required.**
* `:erts_include` — per-target `erts-VSN/include/` dir. **Required.**
* `:out_dir` — where the archive + object subdir get written.
**Required.**
* `:ndk_root` — Android NDK root (Android targets only; defaults to
`~/Library/Android/sdk/ndk/<NdkVersion.effective()>`).
"""
@spec build(atom(), keyword()) :: {:ok, map()} | Errors.t()
def build(target_id, opts \\ [])
when target_id in [:android_arm64, :android_arm32, :ios_sim, :ios_device] do
target = target_spec(target_id)
shell = Shell.impl()
with {:ok, src_root} <- require_opt(opts, :nx_tflite_mob_dir),
{:ok, tflite_dir} <- require_opt(opts, :tflite_dir),
{:ok, erts_inc} <- require_opt(opts, :erts_include),
{:ok, out_dir} <- require_opt(opts, :out_dir) do
src = Path.join([src_root, "c_src", "tflite_nif.c"])
{includes, frameworks} = include_paths(target_id, tflite_dir, erts_inc)
paths = %{
obj_dir: Path.join([out_dir, "obj", arch_dir(target_id)]),
lib_dir: out_dir,
src: src
}
with :ok <- precheck(target, shell, paths, tflite_dir, erts_inc, opts),
tools = target.tools_fn.(opts),
flags = cflags(target, includes, frameworks),
:ok <- shell.mkdir_p(paths.obj_dir),
:ok <- shell.mkdir_p(paths.lib_dir),
obj = Path.join(paths.obj_dir, "tflite_nif.o"),
{:ok, _} <- shell.cmd(tools.cc ++ flags ++ ["-c", "-o", obj, src], []),
archive = Path.join(paths.lib_dir, "libtflite_nif.a"),
:ok <- shell.rm_f(archive),
{:ok, _} <- shell.cmd(tools.ar ++ ["rcs", archive, obj], []),
{:ok, _} <- shell.cmd(tools.ranlib ++ [archive], []),
:ok <- verify_symbol(shell, tools.nm, archive, target.nm_symbol) do
{:ok, %{target: target_id, archive: archive, object: obj}}
end
end
end
# ── Per-target tool / path helpers ──────────────────────────────────────
defp arch_dir(:android_arm64), do: "aarch64-unknown-linux-android"
defp arch_dir(:android_arm32), do: "arm-unknown-linux-androideabi"
defp arch_dir(:ios_sim), do: "aarch64-apple-iossimulator"
defp arch_dir(:ios_device), do: "aarch64-apple-ios"
defp include_paths(target_id, tflite_dir, erts_inc)
when target_id in [:android_arm64, :android_arm32] do
{[
Path.join(tflite_dir, "headers"),
erts_inc,
Path.join(erts_inc, "internal")
], []}
end
defp include_paths(:ios_device, tflite_dir, erts_inc) do
fw_root = Path.join(tflite_dir, "Frameworks")
{[erts_inc, Path.join(erts_inc, "internal")],
[
Path.join([fw_root, "TensorFlowLiteC.xcframework", "ios-arm64"]),
Path.join([fw_root, "TensorFlowLiteCCoreML.xcframework", "ios-arm64"])
]}
end
defp include_paths(:ios_sim, tflite_dir, erts_inc) do
fw_root = Path.join(tflite_dir, "Frameworks")
slice = "ios-arm64_x86_64-simulator"
{[erts_inc, Path.join(erts_inc, "internal")],
[
Path.join([fw_root, "TensorFlowLiteC.xcframework", slice]),
Path.join([fw_root, "TensorFlowLiteCCoreML.xcframework", slice])
]}
end
defp android_tools(opts, arch) do
ndk_root =
opts[:ndk_root] ||
Path.join([System.user_home!(), "Library/Android/sdk/ndk", NdkVersion.effective()])
host = ndk_host()
bin = Path.join([ndk_root, "toolchains/llvm/prebuilt/#{host}/bin"])
cc_name =
case arch do
:android_arm64 -> "aarch64-linux-android#{@android_api}-clang"
:android_arm32 -> "armv7a-linux-androideabi#{@android_api}-clang"
end
%{
cc: [Path.join(bin, cc_name)],
ar: [Path.join(bin, "llvm-ar")],
ranlib: [Path.join(bin, "llvm-ranlib")],
nm: [Path.join(bin, "llvm-nm")]
}
end
defp ios_tools(_opts, target) do
sdk =
case target do
:ios_device -> "iphoneos"
:ios_sim -> "iphonesimulator"
end
sdk_path = String.trim(System.cmd("xcrun", ["--sdk", sdk, "--show-sdk-path"]) |> elem(0))
target_triple =
case target do
:ios_device -> "arm64-apple-ios#{@ios_min_version}"
:ios_sim -> "arm64-apple-ios#{@ios_min_version}-simulator"
end
cc_path = String.trim(System.cmd("xcrun", ["--find", "clang"]) |> elem(0))
ar_path = String.trim(System.cmd("xcrun", ["--find", "ar"]) |> elem(0))
ranlib_path = String.trim(System.cmd("xcrun", ["--find", "ranlib"]) |> elem(0))
nm_path = String.trim(System.cmd("xcrun", ["--find", "nm"]) |> elem(0))
%{
cc: [cc_path, "-arch", "arm64", "-target", target_triple, "-isysroot", sdk_path],
ar: [ar_path],
ranlib: [ranlib_path],
nm: [nm_path]
}
end
defp ndk_host do
case :os.type() do
{:unix, :darwin} -> "darwin-x86_64"
{:unix, :linux} -> "linux-x86_64"
_ -> "darwin-x86_64"
end
end
# ── Precondition + verification ─────────────────────────────────────────
defp precheck(target, shell, paths, tflite_dir, erts_inc, opts) do
cond do
not shell.file?(paths.src) ->
Errors.precondition(
"nx_tflite_mob c_src/tflite_nif.c not found at #{paths.src} — is :nx_tflite_mob in mix deps?"
)
not shell.dir?(tflite_dir) ->
Errors.precondition(
"TFLite bundle missing at #{tflite_dir} — TfliteDownloader.ensure/1 not run for #{target.id}?"
)
not shell.dir?(erts_inc) ->
Errors.precondition("erts include dir missing at #{erts_inc}")
true ->
ensure_toolchain(target, opts)
end
end
defp ensure_toolchain(%Target{id: id}, opts) when id in [:android_arm64, :android_arm32] do
ndk_root =
opts[:ndk_root] ||
Path.join([System.user_home!(), "Library/Android/sdk/ndk", NdkVersion.effective()])
if File.dir?(ndk_root) do
:ok
else
Errors.precondition("Android NDK not found at #{ndk_root}")
end
end
defp ensure_toolchain(%Target{id: id}, _opts) when id in [:ios_sim, :ios_device] do
case System.cmd("xcrun", ["--find", "clang"], stderr_to_stdout: true) do
{_, 0} -> :ok
_ -> Errors.precondition("xcrun / Xcode command-line tools missing")
end
end
defp verify_symbol(shell, nm_cmd, archive, expected) do
case shell.cmd(nm_cmd ++ [archive], []) do
{:ok, out} ->
if String.contains?(out, expected) do
:ok
else
Errors.precondition(
"compile produced libtflite_nif.a without `#{expected}` symbol — STATIC_ERLANG_NIF_LIBNAME=tflite_nif may have been overridden by something upstream"
)
end
err ->
err
end
end
defp require_opt(opts, key) do
case Keyword.fetch(opts, key) do
{:ok, v} -> {:ok, v}
:error -> Errors.precondition("required option missing: #{inspect(key)}")
end
end
end