MobDev.Plugin.CppArchive (mob_dev v0.6.16)

Copy Markdown View Source

Cross-compiles a plugin's lang: :cpp_archive NIF — a set of 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, so a plugin author keeps full control of its own ABI.

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.

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). Pure — silent flag drops are the regression class this whole module guards against, so it's directly testable.

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).

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). Pure — silent flag drops are the regression class this whole module guards against, so it's directly testable.

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.