Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

emmalloc: Add an option to not define the standard exports #20487

Merged
merged 12 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 43 additions & 8 deletions system/lib/emmalloc.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@
* - Debugging and logging directly uses console.log via uses EM_ASM, not
* printf etc., to minimize any risk of debugging or logging depending on
* malloc.
*
* Exporting:
*
* - By default we declare not only emmalloc_malloc, emmalloc_free, etc. but
* also the standard library methods like malloc, free, and some aliases.
* You can override this by defining EMMALLOC_NO_STD_EXPORTS, in which case
* we only declare the emalloc_* ones but not the standard ones.
*/

#include <stdalign.h>
Expand All @@ -63,7 +70,13 @@ static_assert((((int32_t)0x80000000U) >> 31) == -1, "This malloc implementation
#define MALLOC_ALIGNMENT alignof(max_align_t)
static_assert(alignof(max_align_t) == 8, "max_align_t must be correct");

#ifdef EMMALLOC_NO_STD_EXPORTS
#define EMMALLOC_EXPORT
#define EMMALLOC_ALIAS(ALIAS, ORIGINAL)
#else
#define EMMALLOC_EXPORT __attribute__((weak, __visibility__("default")))
#define EMMALLOC_ALIAS(ALIAS, ORIGINAL) extern __typeof(ORIGINAL) ALIAS __attribute__((alias(#ORIGINAL)));
#endif

#define MIN(x, y) ((x) < (y) ? (x) : (y))
#define MAX(x, y) ((x) > (y) ? (x) : (y))
Expand Down Expand Up @@ -812,31 +825,37 @@ void *emmalloc_memalign(size_t alignment, size_t size)
MALLOC_RELEASE();
return ptr;
}
extern __typeof(emmalloc_memalign) emscripten_builtin_memalign __attribute__((alias("emmalloc_memalign")));
EMMALLOC_ALIAS(emscripten_builtin_memalign, emmalloc_memalign);

#ifndef EMMALLOC_NO_STD_EXPORTS
void * EMMALLOC_EXPORT memalign(size_t alignment, size_t size)
{
return emmalloc_memalign(alignment, size);
}
#endif

#ifndef EMMALLOC_NO_STD_EXPORTS
void * EMMALLOC_EXPORT aligned_alloc(size_t alignment, size_t size)
{
if ((alignment % sizeof(void *) != 0) || (size % alignment) != 0)
return 0;
return emmalloc_memalign(alignment, size);
}
#endif

void *emmalloc_malloc(size_t size)
{
return emmalloc_memalign(MALLOC_ALIGNMENT, size);
}
extern __typeof(emmalloc_malloc) emscripten_builtin_malloc __attribute__((alias("emmalloc_malloc")));
extern __typeof(emmalloc_malloc) __libc_malloc __attribute__((alias("emmalloc_malloc")));
EMMALLOC_ALIAS(emscripten_builtin_malloc, emmalloc_malloc);
EMMALLOC_ALIAS(__libc_malloc, emmalloc_malloc);

#ifndef EMMALLOC_NO_STD_EXPORTS
void * EMMALLOC_EXPORT malloc(size_t size)
{
return emmalloc_malloc(size);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

BTW, do you know why we are not using aliases here? Seems like it would be code size and performance win to not go through this wrapper. Perhaps we should make that change as a separate change?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, I just saw this comment now after commenting above on this. I'm not sure why we trampoline here, and as I said there, optimizations remove 99% of the overhead but we do keep a table slot for them, so yeah, this might be worth improving.

#endif

size_t emmalloc_usable_size(void *ptr)
{
Expand All @@ -858,10 +877,12 @@ size_t emmalloc_usable_size(void *ptr)
return size - REGION_HEADER_SIZE;
}

#ifndef EMMALLOC_NO_STD_EXPORTS
size_t EMMALLOC_EXPORT malloc_usable_size(void *ptr)
{
return emmalloc_usable_size(ptr);
}
#endif

void emmalloc_free(void *ptr)
{
Expand Down Expand Up @@ -932,13 +953,15 @@ void emmalloc_free(void *ptr)
emmalloc_validate_memory_regions();
#endif
}
extern __typeof(emmalloc_free) emscripten_builtin_free __attribute__((alias("emmalloc_free")));
extern __typeof(emmalloc_free) __libc_free __attribute__((alias("emmalloc_free")));
EMMALLOC_ALIAS(emscripten_builtin_free, emmalloc_free);
EMMALLOC_ALIAS(__libc_free, emmalloc_free);

#ifndef EMMALLOC_NO_STD_EXPORTS
void EMMALLOC_EXPORT free(void *ptr)
{
return emmalloc_free(ptr);
}
#endif

// Can be called to attempt to increase or decrease the size of the given region
// to a new size (in-place). Returns 1 if resize succeeds, and 0 on failure.
Expand Down Expand Up @@ -1067,10 +1090,12 @@ void *emmalloc_aligned_realloc(void *ptr, size_t alignment, size_t size)
return newptr;
}

#ifndef EMMALLOC_NO_STD_EXPORTS
void * EMMALLOC_EXPORT aligned_realloc(void *ptr, size_t alignment, size_t size)
{
return emmalloc_aligned_realloc(ptr, alignment, size);
}
#endif

// realloc_try() is like realloc(), but only attempts to try to resize the existing memory
// area. If resizing the existing memory area fails, then realloc_try() will return 0
Expand Down Expand Up @@ -1154,12 +1179,14 @@ void *emmalloc_realloc(void *ptr, size_t size)
{
return emmalloc_aligned_realloc(ptr, MALLOC_ALIGNMENT, size);
}
extern __typeof(emmalloc_realloc) __libc_realloc __attribute__((alias("emmalloc_realloc")));
EMMALLOC_ALIAS(__libc_realloc, emmalloc_realloc);

#ifndef EMMALLOC_NO_STD_EXPORTS
void * EMMALLOC_EXPORT realloc(void *ptr, size_t size)
{
return emmalloc_realloc(ptr, size);
}
#endif

// realloc_uninitialized() is like realloc(), but old memory contents
// will be undefined after reallocation. (old memory is not preserved in any case)
Expand All @@ -1177,10 +1204,12 @@ int emmalloc_posix_memalign(void **memptr, size_t alignment, size_t size)
return *memptr ? 0 : 12/*ENOMEM*/;
}

#ifndef EMMALLOC_NO_STD_EXPORTS
int EMMALLOC_EXPORT posix_memalign(void **memptr, size_t alignment, size_t size)
{
return emmalloc_posix_memalign(memptr, alignment, size);
}
#endif

void *emmalloc_calloc(size_t num, size_t size)
{
Expand All @@ -1190,12 +1219,14 @@ void *emmalloc_calloc(size_t num, size_t size)
memset(ptr, 0, bytes);
return ptr;
}
extern __typeof(emmalloc_calloc) __libc_calloc __attribute__((alias("emmalloc_calloc")));
EMMALLOC_ALIAS(__libc_calloc, emmalloc_calloc);

#ifndef EMMALLOC_NO_STD_EXPORTS
void * EMMALLOC_EXPORT calloc(size_t num, size_t size)
{
return emmalloc_calloc(num, size);
}
#endif

static int count_linked_list_size(Region *list)
{
Expand Down Expand Up @@ -1286,12 +1317,14 @@ struct mallinfo emmalloc_mallinfo()
return info;
}

#ifndef EMMALLOC_NO_STD_EXPORTS
struct mallinfo EMMALLOC_EXPORT mallinfo()
{
return emmalloc_mallinfo();
}
#endif

// Note! This function is not fully multithreadin safe: while this function is running, other threads should not be
// Note! This function is not fully multithreading safe: while this function is running, other threads should not be
// allowed to call sbrk()!
static int trim_dynamic_heap_reservation(size_t pad)
{
Expand Down Expand Up @@ -1352,10 +1385,12 @@ int emmalloc_trim(size_t pad)
return success;
}

#ifndef EMMALLOC_NO_STD_EXPORTS
int EMMALLOC_EXPORT malloc_trim(size_t pad)
{
return emmalloc_trim(pad);
}
#endif

size_t emmalloc_dynamic_heap_size()
{
Expand Down
36 changes: 36 additions & 0 deletions test/other/test_emmalloc_in_addition.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include <assert.h>
#include <emscripten/console.h>
#include <emscripten/emmalloc.h>

int main() {
// Verify we can call both malloc and emmalloc_malloc, and that those are
// different functions, unless TEST_EMMALLOC_IS_MALLOC is set (in that case,
// emmalloc is malloc because we let emmalloc define the standard exports like
// malloc).

// We have allocated nothing so far, but there may be some initial allocation
// from startup.
size_t initial = emmalloc_dynamic_heap_size();
emscripten_console_logf("initial: %zu\n", initial);

const size_t ONE_MB = 1024 * 1024;
void* one = malloc(ONE_MB);
assert(one);
#ifndef TEST_EMMALLOC_IS_MALLOC
// We have allocated using malloc, but not emmalloc, so emmalloc reports no
// change in usage.
assert(emmalloc_dynamic_heap_size() == initial);
#else
// malloc == emmalloc_malloc, so emmalloc will report additional usage (of the
Copy link
Collaborator

Choose a reason for hiding this comment

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

You could maybe simplify this test by simply asserting that malloc == emmalloc_malloc as the command says? The you wouldn't need the comment to describe the code as it would more self explanatory?

Copy link
Member Author

Choose a reason for hiding this comment

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

By taking the address, you mean? I wasn't sure that is entirely reliable in general. In particular I'd worry about there being some kind of trampoline but it still going to the same target. (With aliases that wouldn't happen, but we might refactor to use another method.)

Copy link
Collaborator

Choose a reason for hiding this comment

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

If we use another method I think its fine to update this test at that point... but that other method would seem like a bad idea since it would incur a cost.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think there would be a risk that we could forget to update this test... it would be a needle in the haystack of the testsuite, that is, we'd need to somehow realize it needs to be updated then. Most likely we'd forget and it could start to pass incorrectly.

Copy link
Collaborator

Choose a reason for hiding this comment

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

But surely the tests would fail because the assert "malloc == emmalloc" would fail right away, no?

Copy link
Member Author

Choose a reason for hiding this comment

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

In other words, I think it's safer to test this by checking the functionality - that emmalloc actually runs, or does not - and not an indirect property that is connected to that atm, if that makes sense.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Fair enough. This lgtm.

Copy link
Member Author

Choose a reason for hiding this comment

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

I realize you're right that malloc == emmalloc would fail one way at least, if we check both ways (as this PR currently does, so one path checks == and one !=). So my argument isn't as strong as I thought. Still, I think this is a pretty simple way to check the underlying functionality so I do feel it is safer.

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried to add emmalloc == malloc testing as you suggested, because I thought it would be good to have both, but actually that fails atm, it turns out! We do have a trampoline:

void * EMMALLOC_EXPORT malloc(size_t size)
{
return emmalloc_malloc(size);
}

So in a debug build the two are obviously different. It turns out that even in a release build they are, because LLVM allocates two function pointer addresses, one for each. Optimizations (inlining + merging similar functions) end up making them identical, but there are still two function pointers, and the table ends up with two slots with the same function name in it...

It might be worth using an alias rather than a trampoline here, but I'm not sure if there was a reason not to do that. Anyhow, let's leave that separate from this PR, so I'll land this.

Copy link
Member Author

Choose a reason for hiding this comment

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

I can't get emmalloc == malloc testing to work in a followup.

I fixed the code to not have trampolines, and the function pointers are identical, and it works in a debug build, but the LLVM optimizer assumes they are not equal all the time. I seem to remember something about that being undefined behavior, that in some architectures function pointers behave very oddly... so I guess it's that.

// size of the allocation, or perhaps more if it overallocated as an
// optimization).
assert(emmalloc_dynamic_heap_size() >= initial + ONE_MB);
#endif

void* two = emmalloc_malloc(ONE_MB);
assert(two);
// We have allocated using emmalloc, so now emmalloc definitely reports usage.
assert(emmalloc_dynamic_heap_size() >= initial + ONE_MB);

emscripten_console_log("success");
}
15 changes: 15 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -14018,3 +14018,18 @@ def test_hello_world_argv(self):

def test_arguments_global(self):
self.emcc(test_file('hello_world_argv.c'), ['-sENVIRONMENT=web', '-sSTRICT', '--closure=1', '-O2'])

@parameterized({
'no_std_exp': (['-DEMMALLOC_NO_STD_EXPORTS'],),
# When we let emmalloc build with the standard exports like malloc,
# emmalloc == malloc.
'with_std_exp': (['-DTEST_EMMALLOC_IS_MALLOC'],),
})
def test_emmalloc_in_addition(self, args):
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't love the name of this test (or the NO_STD_EXPORTS) but I'm struggling to come up with anything better.

I'm not sure you need the with_std_exp variant of this test since that is the default and tested elsewhere. Can we simplify this test by just verifying that it works as expected in the "NO_EXPORT" case?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe test_emmalloc_explicit or test_emmalloc_standalone? I don't really love them either.

Copy link
Member Author

@kripken kripken Oct 19, 2023

Choose a reason for hiding this comment

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

I considered "standalone", but I felt it is too similar to "standalone wasm" and so it's confusing. I could rename to "explicit" but I think "in addition" is slightly more clear?

I agree with_std_exp is tested elsewhere, but I like that testing both here in relation to that one flag shows the flag exactly controls that behavior. So if we ever get it wrong it won't be a random test that fails, but here. I'd also worry about us removing/modifying the other tests, though that seems like likely.

Copy link
Member Author

Choose a reason for hiding this comment

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

How about "alongside" instead of "in addition" or "explicit"? This does test that we can have emmalloc alongside another malloc at the same time.

# Test that we can use emmalloc in addition to another malloc impl. When we
# build emmalloc using -DEMMALLOC_NO_STD_EXPORTS it will not export malloc
# etc., and only provide the emmalloc_malloc etc. family of functions that
# we can use.
emmalloc = path_from_root('system', 'lib', 'emmalloc.c')
Copy link
Collaborator

Choose a reason for hiding this comment

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

When you build emmalloc in addition to malloc here.. how does it work? How does emmalloc get it underlying memory? Are they both able to grab memory from the sbrk region somehow?

Copy link
Member Author

Choose a reason for hiding this comment

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

Both use sbrk to allocate. Neither frees anything to sbrk, so things work out.

(This won't be an issue for the intended use of this, though, which is that the second allocator calls emmalloc which calls sbrk.)

self.run_process([EMCC, test_file('other/test_emmalloc_in_addition.c'), emmalloc] + args)
self.assertContained('success', self.run_js('a.out.js'))