MobDev.Plugin.CppArchive (mob_dev v0.7.22)

Copy Markdown View Source

Cross-compiles a plugin's lang: :cpp_archive NIF — a set of C/C++ sources — into lib<module>.a for one target ABI, and verifies the NIF-init symbol is present. The archive is then static-linked into the app's single signed native binary (same slot as crypto.a / libnx_eigen.a).

This is the generic, manifest-driven generalization of MobDev.NxEigenNif: the sources, include dirs, CXXFLAGS, and expected symbol all come from the plugin's Merge.static_archives/2 spec rather than being hardcoded. It exists because the single-source :c/:zig plugin NIF path (compiled inline by build.zig) can't express a C++ build with external headers (Eigen/Fine), RTTI/exceptions, a per-arch hardening flag set, and an archive output.

  • Android. A separately-dlopen'd .so inherits RTLD_LOCAL, hiding the BEAM's enif_* symbols → on_load fails. Static-linking lets the BEAM resolve the init symbol via dlsym(RTLD_DEFAULT) on the app binary.
  • iOS. The App Store forbids loading unsigned dylibs / dlopen; the NIF must already be in the signed binary.

What the builder forces vs. what the plugin controls

The builder forces only -fPIC (a static lib linked into a shared object must be position-independent) and the compile/output flags (-c -o). Everything else — C++ standard, optimization, visibility, exceptions, the -DSTATIC_ERLANG_NIF_LIBNAME=… that fixes the emitted init symbol, and any Android hardening — is the plugin's via :cxxflags / :cxxflags_android / :cxxflags_ios (and :cflags* for .c sources), so a plugin author keeps control of its own ABI. One exception: Arm-only flags (-mbranch-protection=) in the Android lists are dropped on :android_x86_64, where clang rejects them, so one Android flag list can serve every ABI.

C and Objective-C sources

A .c or .m entry in :sources is compiled by the target's C driver (clang, not clang++) with :cflags / :cflags_android / :cflags_ios in place of the CXXFLAGS (forced -fPIC, the target ABI flags and the -I includes still apply). Mixed C/C++ libraries such as ggml need this: their .c files are not valid C++. An Objective-C NIF (.m) archived beside an Objective-C++ renderer (.mm) needs it too: clang++ compiles a .m as Objective-C, and a C++ flag such as -std=gnu++17 is then a hard error. Every other extension goes to clang++, which picks the language from the extension (.cpp/.cc C++, .mm Objective-C++).

Object files are named <basename>-<hash of the source path>.o, so two sources with the same basename in different directories (ggml's quants.c and arch/arm/quants.c) both land in the archive. Sources compile in parallel.

Summary

Functions

Archive filename for a NIF module — lib<module>.a.

Compile + archive + verify a cpp_archive spec (from Merge.static_archives/2) for one target_id. Returns {:ok, %{module:, archive:, objects:}} or a tagged error.

Whether a source compiles through the C driver (.c, .m) rather than clang++. Pure.

Assemble the full CFLAGS for one target's .c / .m sources: the same shape as cxxflags/3 (forced -fPIC, target ABI flags, plugin flags, -I includes in order) but reading :cflags and :cflags_android / :cflags_ios, so no C++ flag (-std=c++17, -fno-rtti, …) reaches the C compiler. Pure.

Parse nm output and confirm the expected init symbol is exported (T). Mirrors MobDev.NxEigenNif.check_symbol_present/3. Pure.

Assemble the full CXXFLAGS for one target: forced -fPIC + the target's intrinsic ABI flags (e.g. armv7 flags for android_arm32), then the plugin's base :cxxflags, then the target's platform-specific flags (:cxxflags_android / :cxxflags_ios), then -I for each resolved include dir (order preserved). Arm-only flags (-mbranch-protection=) are dropped on :android_x86_64. Pure — silent flag drops are the regression class this whole module guards against, so it's directly testable.

Object-file name for a source: <basename>-<8 hex of the source path's SHA-256>.o. Stable across builds, and distinct for same-named sources in different directories (a plain <basename>.o let the second overwrite the first, silently dropping its symbols from the archive). Pure.

Resolve a spec's :sources/:includes (a mix of absolute strings and {:dep, name, subpath} tokens left by Merge.static_archives/2) to absolute paths, resolving dep tokens against deps_path. Pure.

All target ABIs a cpp_archive can be built for.

Functions

archive_name(module)

@spec archive_name(atom()) :: String.t()

Archive filename for a NIF module — lib<module>.a.

build(spec, target_id, opts \\ [])

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

Compile + archive + verify a cpp_archive spec (from Merge.static_archives/2) for one target_id. Returns {:ok, %{module:, archive:, objects:}} or a tagged error.

Options:

  • :out_dir — where the archive + per-arch objs land. Required.
  • :erts_include — per-target erts-VSN/include/ dir (carries erl_nif.h). Required.
  • :deps_path — for resolving {:dep, …} include tokens (defaults to Mix.Project.deps_path/0).
  • :ndk_root — Android NDK root (Android targets; default derived).

c_source?(source)

@spec c_source?(Path.t()) :: boolean()

Whether a source compiles through the C driver (.c, .m) rather than clang++. Pure.

cflags(spec, target_id, includes)

@spec cflags(map(), atom(), [Path.t()]) :: [String.t()]

Assemble the full CFLAGS for one target's .c / .m sources: the same shape as cxxflags/3 (forced -fPIC, target ABI flags, plugin flags, -I includes in order) but reading :cflags and :cflags_android / :cflags_ios, so no C++ flag (-std=c++17, -fno-rtti, …) reaches the C compiler. Pure.

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 init symbol is exported (T). Mirrors MobDev.NxEigenNif.check_symbol_present/3. Pure.

cxxflags(spec, target_id, includes)

@spec cxxflags(map(), atom(), [Path.t()]) :: [String.t()]

Assemble the full CXXFLAGS for one target: forced -fPIC + the target's intrinsic ABI flags (e.g. armv7 flags for android_arm32), then the plugin's base :cxxflags, then the target's platform-specific flags (:cxxflags_android / :cxxflags_ios), then -I for each resolved include dir (order preserved). Arm-only flags (-mbranch-protection=) are dropped on :android_x86_64. Pure — silent flag drops are the regression class this whole module guards against, so it's directly testable.

object_name(source)

@spec object_name(Path.t()) :: String.t()

Object-file name for a source: <basename>-<8 hex of the source path's SHA-256>.o. Stable across builds, and distinct for same-named sources in different directories (a plain <basename>.o let the second overwrite the first, silently dropping its symbols from the archive). Pure.

resolve_deps(entries, deps_path)

@spec resolve_deps([Path.t() | {:dep, atom(), String.t()}], Path.t()) :: [Path.t()]

Resolve a spec's :sources/:includes (a mix of absolute strings and {:dep, name, subpath} tokens left by Merge.static_archives/2) to absolute paths, resolving dep tokens against deps_path. Pure.

targets()

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

All target ABIs a cpp_archive can be built for.