Skip to content

erts: Allow erlang:load_nif/2 to load a NIF library from an in-memory binary#10996

Open
saleyn wants to merge 1 commit into
erlang:masterfrom
saleyn:load-so-from-mem
Open

erts: Allow erlang:load_nif/2 to load a NIF library from an in-memory binary#10996
saleyn wants to merge 1 commit into
erlang:masterfrom
saleyn:load-so-from-mem

Conversation

@saleyn
Copy link
Copy Markdown
Contributor

@saleyn saleyn commented Apr 9, 2026

Add support for passing #{memory => NifSO::binary(), filename => Filename::string()} as the first argument to erlang:load_nif/2. When a map is given, the NIF shared object is loaded directly from the binary image in memory instead of from the file system.

Why is this needed?

Presently build tools (i.e. rebar3, mix) cannot bundle all dependencies into a single executable archive escript (a.k.a. escriptizing) that have dependencies with NIF shared object libraries. This PR addresses that gap, and extends the erlang:load_nif/2 call with the missing functionality. After it's merged, the build tools will be able to be modified to take advantage of it. See #4476 issue.

Implementation

  • erts_dlopen_mem() — new static helper in erl_nif.c (Linux/POSIX only). On Linux >= 3.17 it uses memfd_create(2) to create an anonymous in-memory file, writes the SO image into it, and calls dlopen(3) via /proc/self/fd/. On older Linux kernels and other POSIX systems it falls back to shm_open(3) + /dev/shm/. The implementation is derived from the https://github.com/saleyn/memfd_create proof of concept.

  • erts_load_nif() — extended argument parsing:

    • Plain filename :: string()|binary() continues to work unchanged.
    • #{filename => string()|binary()}: same as passing the filename string
    • #{memory => Binary, filename => string()|binary()} map: Binary is extracted via erts_get_aligned_binary_bytes(), passed to erts_dlopen_mem(), and the resulting dlopen handle is used directly. The optional filename string serves only as a label inside the memory-backed file descriptor, and is only valid on non-MacOS systems.
    • The open-from-memory step is performed before the existing else-if validation chain (version checks, module-name check, create_lib) so both code paths share the same validation and bookkeeping.
    • erts_sys_ddll_open() is skipped (via !load_from_mem guard) when a handle was already obtained from memory.
  • The feature is guarded by #if defined(HAVE_DLOPEN) && defined(unix). On unsupported platforms load_nif/2 returns {error,{load_failed,…}}.

Testing

  • nif_SUITE: new test case load_nif_from_mem

    • Happy path: reads nif_mod.1.so into a binary, calls erlang:load_nif(#{memory => Binary, filename => Filename}, []) via nif_mod:load_nif_lib_from_mem/3, and asserts lib_version() == 1.
    • Error path: non-binary second element returns {error,{bad_lib,_}}.
  • nif_mod.erl: new exported helper load_nif_lib_from_mem/3 so that the load_nif call site lives inside the NIF module (required by the BIF).

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

CT Test Results

    3 files    136 suites   52m 23s ⏱️
1 676 tests 1 620 ✅ 56 💤 0 ❌
2 318 runs  2 244 ✅ 74 💤 0 ❌

Results for commit 67cf2fa.

♻️ This comment has been updated with latest results.

To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass.

See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally.

Artifacts

// Erlang/OTP Github Action Bot

@saleyn saleyn force-pushed the load-so-from-mem branch 8 times, most recently from 2ba4729 to 059819e Compare April 10, 2026 06:27
@essen
Copy link
Copy Markdown
Contributor

essen commented Apr 10, 2026

This could be used for Windows: https://github.com/fancycode/MemoryModule

@saleyn saleyn force-pushed the load-so-from-mem branch 3 times, most recently from 23993c1 to 1756f39 Compare April 12, 2026 02:44
@saleyn
Copy link
Copy Markdown
Contributor Author

saleyn commented Apr 12, 2026

This could be used for Windows: https://github.com/fancycode/MemoryModule

After I get the tests pass on all Unix/Mac flavors, I welcome a PR on top of mine to add the Windows support.

@saleyn saleyn force-pushed the load-so-from-mem branch 2 times, most recently from 37183a1 to 8d459f0 Compare April 12, 2026 04:30
@saleyn saleyn changed the title erts: Allow erlang:load_nif/2 to load a NIF from an in-memory binary erts: Allow erlang:load_nif/2 to load a NIF library from an in-memory binary Apr 12, 2026
@saleyn saleyn force-pushed the load-so-from-mem branch from 6832198 to a0d8c34 Compare April 13, 2026 04:08
@rickard-green rickard-green added the team:VM Assigned to OTP team VM label Apr 13, 2026
@jhogberg jhogberg added this to the 30.0 milestone Apr 13, 2026
@jhogberg jhogberg added the stalled waiting for input by the Erlang/OTP team label Apr 13, 2026
@saleyn saleyn force-pushed the load-so-from-mem branch 5 times, most recently from a63e452 to 811316e Compare April 14, 2026 05:56
Add support for passing `#{memory => NifSO::binary(),
filename => string():binary()}` as the first argument to erlang:load_nif/2.
When a map is given with the 'memory' key/value present, the NIF shared
object is loaded directly from the binary image in memory instead of
from the file system. If the map only contains the 'filename' key/value
it'll revert to the default behavior of loading the SO from file.

This addition is needed so that escripts can package SO dependencies
inside and be distributed as a single executable.

Implementation
--------------
* erts_dlopen_mem() — new static helper in erl_nif.c (Linux/POSIX only).
  On Linux >= 3.17 it uses memfd_create(2) to create an anonymous
  in-memory file, writes the SO image into it, and calls dlopen(3) via
  /proc/self/fd/<fd>.  On older Linux kernels and other POSIX systems it
  falls back to shm_open(3) + /dev/shm/<name>.  The implementation is
  derived from the https://github.com/saleyn/memfd_create proof of
  concept.

* erts_load_nif() — extended argument parsing:
  - Plain filename() continues to work unchanged.
  - #{memory => Binary, filename => Filename} map: Binary is extracted via
    erts_get_aligned_binary_bytes(), passed to erts_dlopen_mem(), and
    the resulting dlopen handle is used directly.  The Filename string
    serves only as a label inside the memory-backed file descriptor.
  - The open-from-memory step is performed before the existing else-if
    validation chain (version checks, module-name check, create_lib)
    so both code paths share the same validation and bookkeeping.
  - erts_sys_ddll_open() is skipped (via !load_from_mem guard) when a
    handle was already obtained from memory.

* The feature is guarded by #if defined(HAVE_DLOPEN) && defined(__unix__).
  On unsupported platforms load_nif/2 returns {error,{load_failed,…}}.

Testing
-------
* nif_SUITE: new test case load_nif_from_mem
  - Happy path: reads nif_mod.1.so into a binary, calls
    erlang:load_nif(#{filename => Path, memory => Binary}, []) via
    nif_mod:load_nif_lib_from_mem/3, and asserts lib_version() == 1.
  - Error path: non-binary second element returns {error,{bad_lib,_}}.

* nif_mod.erl: new exported helper load_nif_lib_from_mem/3 so that the
  load_nif call site lives inside the NIF module (required by the BIF).
@saleyn saleyn force-pushed the load-so-from-mem branch from 811316e to 67cf2fa Compare April 14, 2026 19:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stalled waiting for input by the Erlang/OTP team team:VM Assigned to OTP team VM

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants