diff --git a/include/sys/heap_listener.h b/include/sys/heap_listener.h new file mode 100644 index 00000000000..1d2c36d8139 --- /dev/null +++ b/include/sys/heap_listener.h @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_SYS_HEAP_LISTENER_H +#define ZEPHYR_INCLUDE_SYS_HEAP_LISTENER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + + +struct heap_listener { + /** Singly linked list node */ + sys_snode_t node; + + /** + * Identifier of the heap whose events are listened. + * + * It can be a heap pointer, if the heap is represented as an object, + * or 0 in the case of the global libc heap. + */ + uintptr_t heap_id; + + /** Function called when the listened heap is resized */ + void (*resize_cb)(void *old_heap_end, void *new_heap_end); +}; + +/** + * @brief Register heap event listener + * + * Add the listener to the global list of heap listeners that can be notified by + * different heap implementations upon certain events related to the heap usage. + * + * @param listener Pointer to the heap_listener object + */ +void heap_listener_register(struct heap_listener *listener); + +/** + * @brief Unregister heap event listener + * + * Remove the listener from the global list of heap listeners that can be + * notified by different heap implementations upon certain events related to the + * heap usage. + * + * @param listener Pointer to the heap_listener object + */ +void heap_listener_unregister(struct heap_listener *listener); + +/** + * @brief Notify listeners of heap resize event + * + * Notify registered heap event listeners with matching heap identifier that the + * heap has been resized. + * + * @param heap_id Heap identifier + * @param old_heap_end Address of the heap end before the change + * @param new_heap_end Address of the heap end after the change + */ +void heap_listener_notify_resize(uintptr_t heap_id, void *old_heap_end, void *new_heap_end); + +/** + * @brief Construct heap identifier from heap pointer + * + * Construct a heap identifer from a pointer to the heap object, such as + * sys_heap. + * + * @param heap_pointer Pointer to the heap object + */ +#define HEAP_ID_FROM_POINTER(heap_pointer) ((uintptr_t)heap_pointer) + +/** + * @brief Libc heap identifier + * + * Identifier of the global libc heap. + */ +#define HEAP_ID_LIBC ((uintptr_t)0) + +/** + * @brief Define heap event listener object + * + * Sample usage: + * @code + * void on_heap_resized(void *old_heap_end, void *new_heap_end) + * { + * LOG_INF("Libc heap end moved from %p to %p", old_heap_end, new_heap_end); + * } + * + * HEAP_LISTENER_DEFINE(my_listener, HEAP_ID_LIBC, on_heap_resized); + * @endcode + * + * @param name Name of the heap event listener object + * @param _heap_id Identifier of the heap to be listened + * @param _resize_cb Function to be called when the listened heap is resized + */ +#define HEAP_LISTENER_DEFINE(name, _heap_id, _resize_cb) \ + struct heap_listener name = { .heap_id = _heap_id, .resize_cb = _resize_cb } + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_INCLUDE_SYS_HEAP_LISTENER_H */ diff --git a/lib/libc/Kconfig b/lib/libc/Kconfig index 11945961da1..316910736db 100644 --- a/lib/libc/Kconfig +++ b/lib/libc/Kconfig @@ -110,6 +110,13 @@ config NEWLIB_LIBC_FLOAT_SCANF Build with floating point scanf enabled. This will increase the size of the image. +config NEWLIB_LIBC_HEAP_LISTENER + bool "Notify heap listeners of newlib libc heap events" + select HEAP_LISTENER + help + Notify registered heap listeners upon certain events related to the newlib + libc heap usage, such as the heap resize. + endif # NEWLIB_LIBC if MINIMAL_LIBC diff --git a/lib/libc/newlib/libc-hooks.c b/lib/libc/newlib/libc-hooks.c index 74c10dcfd91..f012f909ce9 100644 --- a/lib/libc/newlib/libc-hooks.c +++ b/lib/libc/newlib/libc-hooks.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -291,6 +292,10 @@ void *_sbrk(intptr_t count) if ((heap_sz + count) < MAX_HEAP_SIZE) { heap_sz += count; ret = ptr; + +#ifdef CONFIG_NEWLIB_LIBC_HEAP_LISTENER + heap_listener_notify_resize(HEAP_ID_LIBC, ptr, (char *)ptr + count); +#endif } else { ret = (void *)-1; } diff --git a/lib/os/CMakeLists.txt b/lib/os/CMakeLists.txt index eebae839c03..c7eb825181a 100644 --- a/lib/os/CMakeLists.txt +++ b/lib/os/CMakeLists.txt @@ -45,6 +45,8 @@ zephyr_sources_ifdef(CONFIG_REBOOT reboot.c) zephyr_sources_ifdef(CONFIG_SHARED_MULTI_HEAP shared_multi_heap.c) +zephyr_sources_ifdef(CONFIG_HEAP_LISTENER heap_listener.c) + zephyr_library_include_directories( ${ZEPHYR_BASE}/kernel/include ${ZEPHYR_BASE}/arch/${ARCH}/include diff --git a/lib/os/Kconfig b/lib/os/Kconfig index 720758e3bc9..be25d96daf5 100644 --- a/lib/os/Kconfig +++ b/lib/os/Kconfig @@ -51,6 +51,12 @@ config SYS_HEAP_RUNTIME_STATS help Gather system heap runtime statistics. +config HEAP_LISTENER + bool "Enable heap listener" + help + Enable API for registering and notifying listeners of certain + events related to a heap usage, such as the heap resize. + choice prompt "Supported heap sizes" depends on !64BIT diff --git a/lib/os/heap_listener.c b/lib/os/heap_listener.c new file mode 100644 index 00000000000..b9215ac3589 --- /dev/null +++ b/lib/os/heap_listener.c @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +static struct k_spinlock heap_listener_lock; +static sys_slist_t heap_listener_list = SYS_SLIST_STATIC_INIT(&heap_listener_list); + +void heap_listener_register(struct heap_listener *listener) +{ + k_spinlock_key_t key = k_spin_lock(&heap_listener_lock); + + sys_slist_append(&heap_listener_list, &listener->node); + + k_spin_unlock(&heap_listener_lock, key); +} + +void heap_listener_unregister(struct heap_listener *listener) +{ + k_spinlock_key_t key = k_spin_lock(&heap_listener_lock); + + sys_slist_find_and_remove(&heap_listener_list, &listener->node); + + k_spin_unlock(&heap_listener_lock, key); +} + +void heap_listener_notify_resize(uintptr_t heap_id, void *old_heap_end, void *new_heap_end) +{ + struct heap_listener *listener; + k_spinlock_key_t key = k_spin_lock(&heap_listener_lock); + + SYS_SLIST_FOR_EACH_CONTAINER(&heap_listener_list, listener, node) { + if (listener->heap_id == heap_id && listener->resize_cb != NULL) { + listener->resize_cb(old_heap_end, new_heap_end); + } + } + + k_spin_unlock(&heap_listener_lock, key); +} diff --git a/tests/lib/newlib/heap_listener/CMakeLists.txt b/tests/lib/newlib/heap_listener/CMakeLists.txt new file mode 100644 index 00000000000..5abf38e7ab0 --- /dev/null +++ b/tests/lib/newlib/heap_listener/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(heap_listener) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/lib/newlib/heap_listener/prj.conf b/tests/lib/newlib/heap_listener/prj.conf new file mode 100644 index 00000000000..7282777ff1c --- /dev/null +++ b/tests/lib/newlib/heap_listener/prj.conf @@ -0,0 +1,4 @@ +CONFIG_ZTEST=y +CONFIG_NEWLIB_LIBC=y +CONFIG_NEWLIB_LIBC_NANO=n +CONFIG_NEWLIB_LIBC_HEAP_LISTENER=y diff --git a/tests/lib/newlib/heap_listener/src/main.c b/tests/lib/newlib/heap_listener/src/main.c new file mode 100644 index 00000000000..b72b4442d37 --- /dev/null +++ b/tests/lib/newlib/heap_listener/src/main.c @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2021 Nordic Semiconductor ASA. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include + +/* + * Function used by malloc() to obtain or free memory to the system. + * Returns the heap end before applying the change. + */ +extern void *sbrk(intptr_t count); + +static uintptr_t current_heap_end(void) +{ + return (uintptr_t)sbrk(0); +} + +static ptrdiff_t heap_difference; + +static void heap_resized(void *old_heap_end, void *new_heap_end) +{ + heap_difference += ((char *)new_heap_end - (char *)old_heap_end); +} + +static HEAP_LISTENER_DEFINE(listener, HEAP_ID_LIBC, heap_resized); + +/** + * @brief Test that heap listener is notified when libc heap size changes. + * + * This test calls the malloc() and free() followed by malloc_trim() functions + * and verifies that the heap listener is notified of allocating or returning + * memory from the system. + */ +void test_alloc_and_trim(void) +{ + uintptr_t saved_heap_end; + void *ptr; + + TC_PRINT("Allocating memory...\n"); + + heap_listener_register(&listener); + saved_heap_end = current_heap_end(); + ptr = malloc(4096); + + TC_PRINT("Total heap size change: %zi\n", heap_difference); + + zassert_true(heap_difference > 0, "Heap increase not detected"); + zassert_equal(current_heap_end() - saved_heap_end, heap_difference, + "Heap increase not detected"); + + TC_PRINT("Freeing memory...\n"); + + heap_difference = 0; + saved_heap_end = current_heap_end(); + free(ptr); + malloc_trim(0); + + /* + * malloc_trim() may not free any memory to the system if there is not enough to free. + * Therefore, do not require that heap_difference < 0. + */ + zassert_equal(current_heap_end() - saved_heap_end, heap_difference, + "Heap decrease not detected"); + + heap_listener_unregister(&listener); +} + +void test_main(void) +{ + ztest_test_suite(newlib_libc_heap_listener, + ztest_unit_test(test_alloc_and_trim)); + + ztest_run_test_suite(newlib_libc_heap_listener); +} diff --git a/tests/lib/newlib/heap_listener/testcase.yaml b/tests/lib/newlib/heap_listener/testcase.yaml new file mode 100644 index 00000000000..6c122fd134b --- /dev/null +++ b/tests/lib/newlib/heap_listener/testcase.yaml @@ -0,0 +1,4 @@ +tests: + libraries.libc.newlib.heap_listener: + tags: clib newlib + filter: TOOLCHAIN_HAS_NEWLIB == 1