llext: add context save and restore

Some applications need to save LLEXT context, e.g. when suspending,
to later restore it quickly without a full relinking. Add 2 functions
for context saving and restoring. Since these functions are likely to
change in the future, put them in llext_experimental.c, which depends
on CONFIG_LLEXT_EXPERIMENTAL and is disabled by default.

Signed-off-by: Guennadi Liakhovetski <guennadi.liakhovetski@linux.intel.com>
This commit is contained in:
Guennadi Liakhovetski 2025-06-06 16:38:06 +02:00 committed by Anas Nashif
commit 5f4177b47c
6 changed files with 270 additions and 2 deletions

View file

@ -448,6 +448,53 @@ int llext_heap_init(void *mem, size_t bytes);
* @retval -EBUSY On heap not empty * @retval -EBUSY On heap not empty
*/ */
int llext_heap_uninit(void); int llext_heap_uninit(void);
/**
* @brief Relink dependencies to prepare for suspend
*
* For suspend-resume use-cases, when LLEXT context should be saved in a
* non-volatile buffer, the user can save most LLEXT support data, but they have
* to use @ref llext_restore() to re-allocate objects, which will also have to
* restore dependency pointers. To make sure dependency saving and restoring is
* done consistently, we provide a helper function for the former too.
*
* @warning this is a part of an experimental API, it WILL change in the future!
* Its availability depends on CONFIG_LLEXT_EXPERIMENTAL, which is disabled by
* default.
*
* @param[in] ext Extension array
* @param[in] n_ext Number of extensions
* @retval 0 Success
* @retval -ENOENT Some dependencies not found
*/
int llext_relink_dependency(struct llext *ext, unsigned int n_ext);
/**
* @brief Restore LLEXT context from saved data
*
* During suspend the user has saved all the extension and loader descriptors
* and related objects and called @ref llext_relink_dependency() to prepare
* dependency pointers.
* When resuming llext_alloc() has to be used to re-allocate all the objects,
* therefore the user needs support from LLEXT core to accomplish that.
* This function takes arrays of pointers to saved copies of extensions and
* loaders as arguments and re-allocates all the objects, while also adding them
* to the global extension list. At the same time it relinks dependency pointers
* to newly allocated extensions.
*
* @warning this is a part of an experimental API, it WILL change in the future!
* Its availability depends on CONFIG_LLEXT_EXPERIMENTAL, which is disabled by
* default.
*
* @param[in,out] ext Extension pointer array - replaced with re-allocated copies
* @param[in,out] ldr Array of loader pointers to restore section maps
* @param[in] n_ext Number of extensions
* @retval 0 Success
* @retval -ENOMEM No memory
* @retval -EINVAL Stored dependency out of range
* @retval -EFAULT Internal algorithmic error
*/
int llext_restore(struct llext **ext, struct llext_loader **ldr, unsigned int n_ext);
/** /**
* @} * @}
*/ */

View file

@ -17,6 +17,7 @@ if(CONFIG_LLEXT)
fs_loader.c fs_loader.c
) )
zephyr_library_sources_ifdef(CONFIG_LLEXT_SHELL shell.c) zephyr_library_sources_ifdef(CONFIG_LLEXT_SHELL shell.c)
zephyr_library_sources_ifdef(CONFIG_LLEXT_EXPERIMENTAL llext_experimental.c)
if(CONFIG_RISCV AND CONFIG_USERSPACE) if(CONFIG_RISCV AND CONFIG_USERSPACE)
message(WARNING "Running LLEXT extensions from user-space threads on RISC-V is not supported!") message(WARNING "Running LLEXT extensions from user-space threads on RISC-V is not supported!")

View file

@ -133,6 +133,12 @@ config LLEXT_IMPORT_ALL_GLOBALS
used by the main application. This is useful to load basic extensions used by the main application. This is useful to load basic extensions
that have been compiled without the full Zephyr EDK. that have been compiled without the full Zephyr EDK.
config LLEXT_EXPERIMENTAL
bool "LLEXT experimental functionality"
help
Include support for LLEXT experimental and unstable functionality that
has a very high likelihood to change in the future.
module = LLEXT module = LLEXT
module-str = llext module-str = llext
source "subsys/logging/Kconfig.template.log_config" source "subsys/logging/Kconfig.template.log_config"

View file

@ -19,9 +19,9 @@ LOG_MODULE_REGISTER(llext, CONFIG_LLEXT_LOG_LEVEL);
#include "llext_priv.h" #include "llext_priv.h"
static sys_slist_t llext_list = SYS_SLIST_STATIC_INIT(&llext_list); sys_slist_t llext_list = SYS_SLIST_STATIC_INIT(&llext_list);
static struct k_mutex llext_lock = Z_MUTEX_INITIALIZER(llext_lock); struct k_mutex llext_lock = Z_MUTEX_INITIALIZER(llext_lock);
int llext_section_shndx(const struct llext_loader *ldr, const struct llext *ext, int llext_section_shndx(const struct llext_loader *ldr, const struct llext *ext,
const char *sect_name) const char *sect_name)

View file

@ -0,0 +1,206 @@
/*
* Copyright (c) 2025 Intel Corporation
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/llext/loader.h>
#include <zephyr/llext/llext.h>
#include <zephyr/llext/llext_internal.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/slist.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(llext, CONFIG_LLEXT_LOG_LEVEL);
#include <string.h>
#include "llext_priv.h"
/*
* Prepare a set of extension copies for future restoring. The user has copied
* multiple extensions and their dependencies into a flat array. We have to
* relink dependency pointers to copies in this array for later restoring. Note,
* that all dependencies have to be in this array, if any are missing, restoring
* will fail, so we return an error in such cases.
*/
int llext_relink_dependency(struct llext *ext, unsigned int n_ext)
{
unsigned int i, j, k;
for (i = 0; i < n_ext; i++) {
for (j = 0; ext[i].dependency[j] && j < ARRAY_SIZE(ext[i].dependency); j++) {
for (k = 0; k < n_ext; k++) {
if (!strncmp(ext[k].name, ext[i].dependency[j]->name,
sizeof(ext[k].name))) {
ext[i].dependency[j] = ext + k;
LOG_DBG("backup %s depends on %s",
ext[i].name, ext[k].name);
break;
}
}
if (k == n_ext) {
return -ENOENT;
}
}
}
return 0;
}
int llext_restore(struct llext **ext, struct llext_loader **ldr, unsigned int n_ext)
{
struct llext_elf_sect_map **map = llext_alloc(sizeof(*map) * n_ext);
struct llext *next, *tmp, *first = ext[0], *last = ext[n_ext - 1];
unsigned int i, j, n_map, n_exp_tab;
int ret;
if (!map) {
LOG_ERR("cannot allocate list of maps of %zu", sizeof(*map) * n_ext);
return -ENOMEM;
}
/*
* Each extension has a section map, so if this loop completes
* successfully in the end n_map == n_ext. But if it's terminated
* because of an allocation failure, we need to know how many maps have
* to be freed.
*/
for (n_map = 0, n_exp_tab = 0; n_map < n_ext; n_map++) {
/* Need to allocate individually, because that's how they're freed */
map[n_map] = llext_alloc(sizeof(**map) * ext[n_map]->sect_cnt);
if (!map[n_map]) {
LOG_ERR("cannot allocate section map of %zu",
sizeof(**map) * ext[n_map]->sect_cnt);
ret = -ENOMEM;
goto free_map;
}
/* Not every extension exports symbols, count those that do */
if (ext[n_map]->exp_tab.sym_cnt) {
n_exp_tab++;
}
}
/* Array of pointers to exported symbol tables. Can be NULL if n_exp_tab == 0 */
struct llext_symbol **exp_tab = llext_alloc(sizeof(*exp_tab) * n_exp_tab);
if (n_exp_tab) {
if (!exp_tab) {
LOG_ERR("cannot allocate list of exported symbol tables of %zu",
sizeof(*exp_tab) * n_exp_tab);
ret = -ENOMEM;
goto free_map;
}
/* Now actually allocate new exported symbol lists */
for (i = 0, j = 0; i < n_ext; i++) {
if (ext[i]->exp_tab.sym_cnt) {
size_t size = sizeof(**exp_tab) * ext[i]->exp_tab.sym_cnt;
exp_tab[j] = llext_alloc(size);
if (!exp_tab[j]) {
LOG_ERR("cannot allocate exported symbol table of %zu",
size);
ret = -ENOMEM;
goto free_sym;
}
memcpy(exp_tab[j++], ext[i]->exp_tab.syms, size);
}
}
}
k_mutex_lock(&llext_lock, K_FOREVER);
/* Allocate extensions and add them to the global list */
for (i = 0, j = 0; i < n_ext; i++) {
next = llext_alloc(sizeof(*next));
if (!next) {
LOG_ERR("cannot allocate LLEXT of %zu", sizeof(*next));
ret = -ENOMEM;
goto free_llext;
}
/* Copy the extension and return a pointer to it to the user */
*next = *ext[i];
ext[i] = next;
if (next->exp_tab.sym_cnt) {
next->exp_tab.syms = exp_tab[j++];
}
sys_slist_append(&llext_list, &next->llext_list);
}
k_mutex_unlock(&llext_lock);
/* Copy section maps */
for (i = 0; i < n_ext; i++) {
size_t map_size = sizeof(struct llext_elf_sect_map) * ext[i]->sect_cnt;
memcpy(map[i], ldr[i]->sect_map, map_size);
ldr[i]->sect_map = map[i];
}
llext_free(map);
llext_free(exp_tab);
/* Restore dependencies previously saved by llext_relink_dependency() */
SYS_SLIST_FOR_EACH_CONTAINER(&llext_list, next, llext_list) {
for (j = 0; next->dependency[j] && j < ARRAY_SIZE(next->dependency); j++) {
if (next->dependency[j] < first || next->dependency[j] >= last) {
/* Inconsistency detected */
LOG_ERR("dependency out of range");
ret = -EINVAL;
goto free_locked;
}
next->dependency[j] = llext_by_name(next->dependency[j]->name);
if (!next->dependency[j]) {
/* Bug in the algorithm */
LOG_ERR("dependency not found");
ret = -EFAULT;
goto free_locked;
}
LOG_DBG("restore %s depends on %s",
next->name, next->dependency[j]->name);
}
}
return 0;
free_locked:
k_mutex_lock(&llext_lock, K_FOREVER);
free_llext:
/* Free only those extensions, that we've saved into ext[] */
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&llext_list, next, tmp, llext_list) {
for (i = 0; i < n_ext; i++) {
if (ext[i] == next) {
sys_slist_remove(&llext_list, NULL, &next->llext_list);
llext_free(next);
ext[i] = NULL;
break;
}
}
}
k_mutex_unlock(&llext_lock);
free_sym:
for (i = 0; i < n_exp_tab && exp_tab[i]; i++) {
llext_free(exp_tab[i]);
}
llext_free(exp_tab);
free_map:
for (i = 0; i < n_map; i++) {
llext_free(map[i]);
}
llext_free(map);
return ret;
}

View file

@ -10,6 +10,14 @@
#include <zephyr/kernel.h> #include <zephyr/kernel.h>
#include <zephyr/llext/llext.h> #include <zephyr/llext/llext.h>
#include <zephyr/llext/llext_internal.h> #include <zephyr/llext/llext_internal.h>
#include <zephyr/sys/slist.h>
/*
* Global extension list
*/
extern sys_slist_t llext_list;
extern struct k_mutex llext_lock;
/* /*
* Memory management (llext_mem.c) * Memory management (llext_mem.c)