Skip to content

Add optional runtime swappable custom allocator#2230

Open
schloing wants to merge 2 commits intonanomsg:mainfrom
schloing:main
Open

Add optional runtime swappable custom allocator#2230
schloing wants to merge 2 commits intonanomsg:mainfrom
schloing:main

Conversation

@schloing
Copy link

@schloing schloing commented Mar 13, 2026

Ability to override allocation functions with function pointers nni_malloc_fn, nni_calloc_fn and nni_free_fn. These pointers can be updated at runtime using nng_alloc_set.

By default, they are set to malloc, calloc and free. Existing NNG API behaviour not impacted.

Functionality disabled by default, and is enabled with CMake option NNG_ENABLE_CUSTOM_ALLOC. When NNG_ENABLE_CUSTOM_ALLOC is off,
the function pointers are not used, and nni_alloc, nni_zalloc, and
nni_free call the corresponding OS functions directly.

Summary by CodeRabbit

  • New Features
    • Added configurable memory allocation support with a new CMake option (disabled by default) enabling users to override default allocation functions.
    • New public API function to set custom memory allocation hooks for malloc, calloc, and free operations.

@schloing schloing requested a review from gdamore as a code owner March 13, 2026 13:33
@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

The pull request introduces a configurable custom memory allocation feature via the NNG_ENABLE_CUSTOM_ALLOC compile-time flag. A new public API function nng_alloc_set() allows users to install custom malloc, calloc, and free function pointers that override the default allocators. The implementation uses function pointer indirection in platform-specific allocation code.

Changes

Cohort / File(s) Summary
Build Configuration
CMakeLists.txt, cmake/NNGOptions.cmake
Added new CMake option NNG_ENABLE_CUSTOM_ALLOC (default OFF) with conditional preprocessor define invocation in build system.
Public API Header
include/nng/nng.h
Added new public function declaration void nng_alloc_set(void* (*malloc)(size_t), void* (*calloc)(size_t, size_t), void (*free)(void*)) guarded by #ifdef NNG_ENABLE_CUSTOM_ALLOC.
Core Infrastructure
src/core/defs.h
Added three extern function pointer declarations: nni_malloc_fn, nni_calloc_fn, and nni_free_fn to enable allocator indirection.
Public API Implementation
src/nng.c
Implemented nng_alloc_set() function to configure allocator function pointers conditionally, with include of core/defs.h.
Platform Allocators
src/platform/posix/posix_alloc.c, src/platform/windows/win_thread.c
Refactored allocation functions (nni_alloc, nni_zalloc, nni_free) to use function pointer indirection instead of direct calls to malloc/calloc/free when NNG_ENABLE_CUSTOM_ALLOC is enabled.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Hop hop, allocate with care,
Custom pointers float through the air,
No more malloc's iron chain,
Our memory flows down custom lane!
Configuration sets us free,
To allocate however we please, you see!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main feature: adding a runtime-swappable custom allocator functionality that is optional.
Description check ✅ Passed The description explains what the feature does, how to enable it, and confirms existing behavior is unaffected, but does not reference an issue number as specified in the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can enable review details to help with troubleshooting, context usage and more.

Enable the reviews.review_details setting to include review details such as the model used, the time taken for each step and more in the review comments.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@include/nng/nng.h`:
- Around line 554-556: The declaration of nng_alloc_set is currently guarded by
private macro NNG_ENABLE_CUSTOM_ALLOC which can hide the public API; always
expose the nng_alloc_set declaration (remove the conditional) and implement a
stub/error fallback in the library when custom alloc is not enabled, or
alternatively move the enabling macro into the generated public config and use
that public macro for the guard so consumers and tests see consistent
declarations; update the implementation (function body) to either call the
provided pointers when enabled or return/abort with a clear error when custom
alloc is not supported, ensuring tests can still link against the symbol.

In `@src/nng.c`:
- Around line 51-66: The allocator setters (nng_alloc_set) currently update
nni_malloc_fn, nni_calloc_fn, and nni_free_fn independently and without
synchronization while platform allocators (posix_alloc.c and win_thread.c) read
them unsafely; change nng_alloc_set to be a one-shot, atomic installation that
must be called before any NNG allocations: require the caller to install all
three callbacks together, make nng_alloc_set return an error code (e.g., int) if
called after initialization/allocation has started, and perform a single atomic
swap or guarded check to set all three function pointers at once (or reject the
call). Alternatively, embed the matching free callback into each allocation path
(store the free pointer with the allocation metadata) so that free uses the
allocator that performed the allocation; update references to
nni_malloc_fn/nni_calloc_fn/nni_free_fn accordingly and ensure posix_alloc.c and
win_thread.c use the new safe mechanism.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0b2ade47-e511-44b6-97b2-435e051df56e

📥 Commits

Reviewing files that changed from the base of the PR and between 78a54cb and 0297a04.

📒 Files selected for processing (7)
  • CMakeLists.txt
  • cmake/NNGOptions.cmake
  • include/nng/nng.h
  • src/core/defs.h
  • src/nng.c
  • src/platform/posix/posix_alloc.c
  • src/platform/windows/win_thread.c

Comment on lines +554 to +556
#ifdef NNG_ENABLE_CUSTOM_ALLOC
NNG_DECL void nng_alloc_set(void* (*malloc)(size_t), void* (*calloc)(size_t, size_t), void (*free)(void*));
#endif
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't hide a public API behind NNG_ENABLE_CUSTOM_ALLOC.

With the current wiring, NNG_ENABLE_CUSTOM_ALLOC is private to nng and always public on nng_testing, so enabled builds can hide nng_alloc_set() from real consumers while OFF builds still expose it to tests. This declaration needs to be always visible with a stub/error implementation behind it, or the macro needs to come from a generated public config header that is exported to consumers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/nng/nng.h` around lines 554 - 556, The declaration of nng_alloc_set
is currently guarded by private macro NNG_ENABLE_CUSTOM_ALLOC which can hide the
public API; always expose the nng_alloc_set declaration (remove the conditional)
and implement a stub/error fallback in the library when custom alloc is not
enabled, or alternatively move the enabling macro into the generated public
config and use that public macro for the guard so consumers and tests see
consistent declarations; update the implementation (function body) to either
call the provided pointers when enabled or return/abort with a clear error when
custom alloc is not supported, ensuring tests can still link against the symbol.

Comment on lines +51 to +66
#ifdef NNG_ENABLE_CUSTOM_ALLOC
void
nng_alloc_set(void* (*malloc)(size_t), void* (*calloc)(size_t, size_t), void (*free)(void*))
{
if (malloc) {
nni_malloc_fn = malloc;
}

if (calloc) {
nni_calloc_fn = calloc;
}

if (free) {
nni_free_fn = free;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.c" | grep -E "(posix_alloc|win_thread|nng\.c)" | head -20

Repository: nanomsg/nng

Length of output: 137


🏁 Script executed:

# Check the exact location of src/nng.c and the nng_alloc_set function
fd "nng\.c$" --type f

Repository: nanomsg/nng

Length of output: 64


🏁 Script executed:

# List the structure to understand the layout
ls -la src/ 2>/dev/null | head -20

Repository: nanomsg/nng

Length of output: 707


🏁 Script executed:

# Read src/nng.c around lines 51-66 to see the nng_alloc_set function
sed -n '1,80p' src/nng.c | cat -n

Repository: nanomsg/nng

Length of output: 2379


🏁 Script executed:

# Search for the global function pointer declarations
rg "nni_malloc_fn|nni_calloc_fn|nni_free_fn" src/nng.c -B 2 -A 2 -n

Repository: nanomsg/nng

Length of output: 225


🏁 Script executed:

# Check posix_alloc.c to see how globals are used
cat -n src/platform/posix/posix_alloc.c

Repository: nanomsg/nng

Length of output: 1365


🏁 Script executed:

# Check win_thread.c to see allocator usage
sed -n '1,100p' src/platform/windows/win_thread.c | cat -n

Repository: nanomsg/nng

Length of output: 2973


🏁 Script executed:

# Check core/nng_impl.h or core/defs.h for declarations and synchronization
grep -r "nni_malloc_fn\|nni_calloc_fn\|nni_free_fn" src/core/ -B 2 -A 2 -n

Repository: nanomsg/nng

Length of output: 463


🏁 Script executed:

# Search for any mutex or synchronization around allocator setting
rg "nng_alloc_set|nni_malloc_fn|nni_calloc_fn|nni_free_fn" src/ --type c -B 3 -A 3 | head -100

Repository: nanomsg/nng

Length of output: 4318


🏁 Script executed:

# Check if there's any initialization guard or documentation
rg "ENABLE_CUSTOM_ALLOC" src/ -B 5 -A 5 | head -150

Repository: nanomsg/nng

Length of output: 1662


🏁 Script executed:

# Check if there's any documentation about intended usage or timing
grep -r "nng_alloc_set\|custom.*alloc" src/ --type c --type h -B 2 -A 2 | grep -i "comment\|doc\|usage\|before\|after" | head -30

Repository: nanomsg/nng

Length of output: 172


🏁 Script executed:

# Check if there are any thread synchronization mechanisms or atomic operations used
rg "atomic|mutex|lock|synchroni" src/core/defs.h -B 3 -A 3

Repository: nanomsg/nng

Length of output: 310


🏁 Script executed:

# Verify that the globals are truly not protected by any locks
rg "static.*nni_malloc_fn|static.*nni_calloc_fn|static.*nni_free_fn" src/ -B 5 -A 5

Repository: nanomsg/nng

Length of output: 37


🏁 Script executed:

# Check if there's any documentation about nng_alloc_set in header files
rg "nng_alloc_set" src/ --type c --type h -B 5 -A 5

Repository: nanomsg/nng

Length of output: 389


🏁 Script executed:

# Look for any public header that might document expected usage
fd "\.h$" src/ -x grep -l "nng_alloc_set" {}

Repository: nanomsg/nng

Length of output: 37


🏁 Script executed:

# Check if initialization happens before any allocations
rg "nng_init|nng_open_pipe|nng_socket" src/nng.c -A 5 | head -50

Repository: nanomsg/nng

Length of output: 966


🏁 Script executed:

# Look for README or documentation files mentioning custom allocators
fd -e md -e rst -e txt . | xargs grep -l "custom.*alloc\|nng_alloc_set" 2>/dev/null

Repository: nanomsg/nng

Length of output: 39


🏁 Script executed:

# Check git history or comments in the file for intent
head -100 src/nng.c | grep -A 10 -B 10 "NNG_ENABLE_CUSTOM_ALLOC"

Repository: nanomsg/nng

Length of output: 396


🏁 Script executed:

# Verify if allocations happen within socket/pipe creation (confirming early initialization would be required)
rg "nni_alloc|nni_zalloc" src/core/ --type c | head -30

Repository: nanomsg/nng

Length of output: 2114


🏁 Script executed:

# Double-check: Are the globals truly extern in defs.h, confirming they are shared across compilation units?
cat -n src/core/defs.h | sed -n '245,260p'

Repository: nanomsg/nng

Length of output: 732


🏁 Script executed:

# Verify there is no initialization guard or one-shot check anywhere
rg "nng_alloc_set|nni_malloc_fn.*=" src/nng.c -B 10 -A 10

Repository: nanomsg/nng

Length of output: 401


🏁 Script executed:

# Check if there's any documentation or comments about thread-safety in platform headers
cat -n src/core/platform.h | grep -A 10 -B 10 "alloc"

Repository: nanomsg/nng

Length of output: 4921


🏁 Script executed:

# Final verification: confirm multiple definitions in different compilation units
rg "void\* \(\*nni_malloc_fn\)" src/ --type c

Repository: nanomsg/nng

Length of output: 258


🏁 Script executed:

# Verify no atomic or synchronization wrapper exists around the setter
rg "static|atomic|volatile|lock|mutex" src/nng.c | grep -A 5 -B 5 "nng_alloc_set"

Repository: nanomsg/nng

Length of output: 37


Synchronize and restrict allocator changes to one-shot initialization.

The globals nni_malloc_fn, nni_calloc_fn, and nni_free_fn are read without synchronization on every alloc/free in src/platform/posix/posix_alloc.c (lines 30, 36, 43) and src/platform/windows/win_thread.c (lines 52, 58, 65). Calling nng_alloc_set() after NNG allocations have begun risks freeing old allocations through a different free callback than the one used for allocation, causing heap mismatch. The independent, partial updates (each function pointer checked separately for NULL) worsen the race window. The void return type prevents callers from detecting or reacting to unsafe timing. Require allocator installation before any NNG allocation occurs, or embed the matching free callback with each allocation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nng.c` around lines 51 - 66, The allocator setters (nng_alloc_set)
currently update nni_malloc_fn, nni_calloc_fn, and nni_free_fn independently and
without synchronization while platform allocators (posix_alloc.c and
win_thread.c) read them unsafely; change nng_alloc_set to be a one-shot, atomic
installation that must be called before any NNG allocations: require the caller
to install all three callbacks together, make nng_alloc_set return an error code
(e.g., int) if called after initialization/allocation has started, and perform a
single atomic swap or guarded check to set all three function pointers at once
(or reject the call). Alternatively, embed the matching free callback into each
allocation path (store the free pointer with the allocation metadata) so that
free uses the allocator that performed the allocation; update references to
nni_malloc_fn/nni_calloc_fn/nni_free_fn accordingly and ensure posix_alloc.c and
win_thread.c use the new safe mechanism.

Copy link
Contributor

@gdamore gdamore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few things here:

  1. This shouldn't be conditionalized in CMAKE. We can do it all the time. Compared to the overhead of actually allocating memory, the function pointer dereference is nothing in terms of performance.
  2. The allocators should be moved to an nng_init parameter, without a separate function. Most notably it would be very bad for the application to change the allocators during a run -- this could also have bad ramifications if you wind up having for example two different libraries, both running on NNG, but doing this separately (thereby stomping on each other.)

Originally I assumed that changing the allocators would be done as part of a platform port, but I can see other uses here, so I'm supportive of this change in general. Thanks for contributing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants