From 99111aeddabdb89a6a2fdb6fdef87df716d4cea4 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Fri, 23 Oct 2026 18:00:50 +0900 Subject: [PATCH 1/5] support zephyr --- justfile | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/justfile b/justfile index 2381ea5..7a61153 100644 --- a/justfile +++ b/justfile @@ -94,6 +94,18 @@ zephyr-run: zephyr-shell: zephyr/docker.sh shell +# Build the examples for Zephyr native_sim inside Docker (EXAMPLE= for one) +zephyr-build: + zephyr/docker.sh build + +# Run the Zephyr examples on native_sim +zephyr-run: + zephyr/docker.sh run + +# Open a shell in the Zephyr container +zephyr-shell: + zephyr/docker.sh shell + # Remove the default build directory clean: rm -rf {{build_dir}} From 9806d44f323bcd84e65dbff4d4d4b7b60cf92587 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Sat, 24 Oct 2026 05:48:09 +0900 Subject: [PATCH 2/5] change posix to linux backend --- CMakeLists.txt | 12 +-- README.md | 4 +- docs/README.md | 2 +- docs/architecture.md | 6 +- docs/lifetime.md | 2 +- docs/porting.md | 18 ++-- docs/thread-safety.md | 3 +- justfile | 2 +- source/{posix => linux}/osal_mutex.c | 0 source/{posix => linux}/osal_queue.c | 0 source/{posix => linux}/osal_sem.c | 0 source/{posix => linux}/osal_task.c | 0 source/{posix => linux}/osal_time.c | 0 source/{posix => linux}/osal_timer.c | 0 source/osal_tmcheck.c | 132 +++++++++++++++++---------- source/zephyr/osal_queue.c | 2 +- test/osal/tmcheck_test.c | 57 +++++++++++- zephyr/README.md | 4 +- 18 files changed, 167 insertions(+), 77 deletions(-) rename source/{posix => linux}/osal_mutex.c (100%) rename source/{posix => linux}/osal_queue.c (100%) rename source/{posix => linux}/osal_sem.c (100%) rename source/{posix => linux}/osal_task.c (100%) rename source/{posix => linux}/osal_time.c (100%) rename source/{posix => linux}/osal_timer.c (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 65e4880..542db1b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,12 +78,12 @@ set(osal_SRC source/osal_log.c source/osal_rm.c source/osal_tmcheck.c - source/posix/osal_mutex.c - source/posix/osal_queue.c - source/posix/osal_sem.c - source/posix/osal_task.c - source/posix/osal_time.c - source/posix/osal_timer.c + source/linux/osal_mutex.c + source/linux/osal_queue.c + source/linux/osal_sem.c + source/linux/osal_task.c + source/linux/osal_time.c + source/linux/osal_timer.c ) # Generated headers live under the build tree — never in the source tree. diff --git a/README.md b/README.md index c63bfcd..d03ad04 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ semantics across POSIX and RTOS platforms. Everything (tasks, mutexes, semaphores, queues, timers, ...) is allocated statically up front through a central resource manager, so there's no malloc on the hot path and the worst-case memory usage is known ahead of time. A -POSIX/Linux backend and a Zephyr backend are included; other targets can be +Linux backend and a Zephyr backend are included; other targets can be added behind the same API without breaking existing callers. ``` @@ -87,7 +87,7 @@ backend maps to Zephyr primitives. ## Examples Sample programs live in [examples/](examples/). Build them with `just build` -(CMake target `examples`) for POSIX, or with `zephyr/docker.sh build` for +(CMake target `examples`) for Linux, or with `zephyr/docker.sh build` for Zephyr. ## Documentation diff --git a/docs/README.md b/docs/README.md index 0293d82..2479012 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ Doxygen-generated API reference (built via `make doc`). | [architecture.md](architecture.md) | Layered design, subsystem overview, resource-manager pattern, callback threading model, backend model. | | [api-spec.md](api-spec.md) | Behavioral specification per subsystem — functions, states, error codes, invariants. | | [lifetime.md](lifetime.md) | Init/deinit ordering, resource ownership, when it is safe to use / release a resource. | -| [porting.md](porting.md) | How to add a new backend (e.g. an RTOS or bare-metal target). Reference: the POSIX backend. | +| [porting.md](porting.md) | How to add a new backend (e.g. an RTOS or bare-metal target). Reference: the Linux backend. | | [thread-safety.md](thread-safety.md) | Per-API table classifying every public call as thread-safe, single-owner, or init-thread-only. | If you're just getting started, read them in this order: diff --git a/docs/architecture.md b/docs/architecture.md index dbc2852..0edd6cf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,7 +18,7 @@ behaviour matters more than dynamic flexibility. | dmosal (public API) | +---------------+---------------+ | portable | backend | - | layer | (POSIX...) | + | layer | (Linux...) | +---------------+---------------+ | OS user-space API | +-------------------------------+ @@ -34,7 +34,7 @@ behaviour matters more than dynamic flexibility. across every backend (resource manager, LIFO free-list, error strings, logging, time-checkpoint utility, top-level init). - **Backend** — `source//*.c`. Only these files touch OS-specific - APIs. The POSIX backend under `source/posix/` is the reference + APIs. The Linux backend under `source/linux/` is the reference implementation. ## Subsystems @@ -93,7 +93,7 @@ Two subsystems invoke user code from a thread other than the caller's: - **Task** — `osal_task_create(cfg)` spawns a new thread that runs `cfg->task_handler`. The handler runs to completion on that thread. - **Timer** — the timer callback fires on a backend-managed context, not - the caller's thread. On the POSIX backend that means a fresh helper + the caller's thread. On the Linux backend that means a fresh helper thread per expiration (via `SIGEV_THREAD`). User code must not assume any particular thread identity inside the callback. diff --git a/docs/lifetime.md b/docs/lifetime.md index 0b5937f..18d87bf 100644 --- a/docs/lifetime.md +++ b/docs/lifetime.md @@ -133,7 +133,7 @@ create -> [started/stopped, callback fires 0+ times] -> stop -> delete create --opens or attaches--> [send/recv N times] --unlinks if creator--> delete ``` -- On the POSIX backend, `osal_queue_create` may either create the +- On the Linux backend, `osal_queue_create` may either create the underlying kernel queue or attach to an existing one with the same name. `osal_queue_delete` unlinks the kernel queue only when this handle originally created it. diff --git a/docs/porting.md b/docs/porting.md index e3608a4..5d3c1e3 100644 --- a/docs/porting.md +++ b/docs/porting.md @@ -5,7 +5,7 @@ under `source/`. The rest of the library — the resource manager, LIFO, logging, error strings, tmcheck, and top-level init — is portable C and unchanged across backends. -The reference implementation lives at `source/posix/` and targets Linux / +The reference implementation lives at `source/linux/` and targets Linux / glibc. This document walks through what a new backend must provide. ## Files a backend must implement @@ -31,7 +31,7 @@ For each function, honour the behavior documented in [lifetime.md](lifetime.md). Concretely: - **Pool ops must be thread-safe** — `_create` and `_delete` need to - serialize their pool touches. On POSIX we do this by threading the + serialize their pool touches. On Linux we do this by threading the shared `osal_mutex_t *mutex` through each `_man_t`'s `osal_rm_t`. Reuse that pattern via `OSAL_RM_USEROBJMAN_DECLARE` / `_INIT` if the target OS has a mutex primitive. @@ -43,7 +43,7 @@ For each function, honour the behavior documented in without a helper-thread mechanism, provide one (e.g. a dedicated timer dispatch task). - **`osal_timer_delete` must reject in-callback invocation.** Use the - `in_callback` bool pattern from the POSIX backend: set true around the + `in_callback` bool pattern from the Linux backend: set true around the callback invocation, `OSAL_RUNTIME_ASSERT` in delete if it's true. ## Steps @@ -61,13 +61,13 @@ For each function, honour the behavior documented in 2. **Add sources to `CMakeLists.txt`.** Extend the `osal_SRC` list to include `source/mybackend/*.c` when the target backend is selected. - The simplest scheme: add an option `DMOSAL_BACKEND=posix|mybackend` + The simplest scheme: add an option `DMOSAL_BACKEND=linux|mybackend` and switch the source glob accordingly. Only one backend is compiled in at a time. 3. **Implement `osal_mutex.c` first.** Everything else depends on it — the shared mutex from this pool is passed to every other subsystem's - init. See `source/posix/osal_mutex.c` for the reference: + init. See `source/linux/osal_mutex.c` for the reference: - `s_mutex_man` static holding the pool. - A bare backend-level mutex (`s_mutex_man.resrc_mutex`) is used inside `osal_mutex_create` because it can't recursively use its own @@ -77,11 +77,11 @@ For each function, honour the behavior documented in 4. **Implement `osal_sem.c`, `osal_task.c`, `osal_timer.c`, `osal_queue.c`, `osal_time.c`.** Each mirrors the same `_man_t` + `osal_rm_t` structure. - Copy the POSIX file and swap the OS calls. + Copy the Linux file and swap the OS calls. 5. **Wire in the build.** `CMakeLists.txt` is the only place where the backend directory is referenced. Neither the public headers nor the - portable `source/*.c` files hard-code POSIX. + portable `source/*.c` files hard-code Linux. 6. **Run the existing test suite.** All six cmocka tests exercise only the public API and should pass unchanged. If they fail, the failure @@ -96,8 +96,8 @@ Some behaviors are inherently backend-specific and may need adjustment: the microsecond field will just always show 0 — no code change needed. - **`osal_task_cfg_t` hints** — `stack_addr`, `stack_size`, `priority`, `name` are best-effort. Honour them if your target OS supports them. - The POSIX reference implementation ignores them. -- **Timer callback thread** — the POSIX backend spawns one helper thread + The Linux reference implementation ignores them. +- **Timer callback thread** — the Linux backend spawns one helper thread per expiration (`SIGEV_THREAD`). On memory-constrained targets, use a single dedicated timer-dispatch task that walks a pending list. diff --git a/docs/thread-safety.md b/docs/thread-safety.md index 682bf86..ecd19fd 100644 --- a/docs/thread-safety.md +++ b/docs/thread-safety.md @@ -123,7 +123,8 @@ generally safe on most systems. |---|---| | `osal_tmcheck_init`, `osal_tmcheck_deinit` | **[init]** | | `osal_tmcheck_create`, `osal_tmcheck_delete` | **[any]** — internally locked | -| `osal_tmcheck_capture_ts`, `osal_tmcheck_reset`, `osal_tmcheck_reset_all`, all `osal_tmcheck_*_print*`, `osal_tmcheck_get_diff`, `osal_tmcheck_name_get_diff`, `osal_tmcheck_get_captured_ts`, `osal_tmcheck_name_get_captured_ts` | **[owner]** — diagnostic-only paths intentionally unlocked. Data may be slightly inconsistent if racing against `_delete` | +| all `osal_tmcheck_*_print*`, `osal_tmcheck_name_get_diff`, `osal_tmcheck_name_get_captured_ts` | **[any]** — cold path, internally locked (names and the in-use flag are read under the shared mutex) | +| `osal_tmcheck_capture_ts`, `osal_tmcheck_reset`, `osal_tmcheck_reset_all`, `osal_tmcheck_get_diff`, `osal_tmcheck_get_captured_ts` | **[owner]** — hot path, intentionally lock-free so measurement is not perturbed; the caller serializes access to a given checkpoint. Data may be slightly inconsistent if racing against `_delete` | | `osal_tmcheck_use`, `osal_tmcheck_avail` | **[any]** | ## Assert diff --git a/justfile b/justfile index 7a61153..f836cad 100644 --- a/justfile +++ b/justfile @@ -76,7 +76,7 @@ doc: configure format: pre-commit run clang-format --all-files -# Run clang-tidy over the portable + POSIX sources via pre-commit +# Run clang-tidy over the portable + Linux sources via pre-commit tidy: cmake -S . -B {{build_dir}} -DCMAKE_BUILD_TYPE={{build_type}} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON cmake --build {{build_dir}} --target dmosal -j{{jobs}} diff --git a/source/posix/osal_mutex.c b/source/linux/osal_mutex.c similarity index 100% rename from source/posix/osal_mutex.c rename to source/linux/osal_mutex.c diff --git a/source/posix/osal_queue.c b/source/linux/osal_queue.c similarity index 100% rename from source/posix/osal_queue.c rename to source/linux/osal_queue.c diff --git a/source/posix/osal_sem.c b/source/linux/osal_sem.c similarity index 100% rename from source/posix/osal_sem.c rename to source/linux/osal_sem.c diff --git a/source/posix/osal_task.c b/source/linux/osal_task.c similarity index 100% rename from source/posix/osal_task.c rename to source/linux/osal_task.c diff --git a/source/posix/osal_time.c b/source/linux/osal_time.c similarity index 100% rename from source/posix/osal_time.c rename to source/linux/osal_time.c diff --git a/source/posix/osal_timer.c b/source/linux/osal_timer.c similarity index 100% rename from source/posix/osal_timer.c rename to source/linux/osal_timer.c diff --git a/source/osal_tmcheck.c b/source/osal_tmcheck.c index 82cc653..f75718d 100644 --- a/source/osal_tmcheck.c +++ b/source/osal_tmcheck.c @@ -165,15 +165,68 @@ void osal_tmcheck_capture_ts(int idx) } } +/* + * Cold-path functions (print and name lookup) take the lock because they read + * names / the inuse flag that create and delete modify. capture_ts, get_captured_ts, + * reset, reset_all and get_diff are the hot path and stay lock-free: the caller serializes + * access to a given checkpoint. + */ +static void snapshot_all(tmcheck_t *dst) +{ + TMCHECK_LOCK(); + memcpy(dst, s_tmcheck_man.tmchecks, sizeof(s_tmcheck_man.tmchecks)); + TMCHECK_UNLOCK(); +} + +static bool snapshot_idx(int idx, tmcheck_t *dst) +{ + if (validate_idx(idx) == false) { + return false; + } + TMCHECK_LOCK(); + *dst = s_tmcheck_man.tmchecks[idx]; + TMCHECK_UNLOCK(); + return true; +} + +/* Look up two names under the lock; returns true when both are found. */ +static bool snapshot_names(const char *name1, const char *name2, tmcheck_t *dst1, tmcheck_t *dst2) +{ + bool found1 = false; + bool found2 = false; + int i; + + TMCHECK_LOCK(); + for (i = 0; i < OSAL_TMCHECK_NUM_MAX; i++) { + const tmcheck_t *entry = &s_tmcheck_man.tmchecks[i]; + + if (entry->inuse == 0) { + continue; + } + if (!found1 && strcmp(entry->name, name1) == 0) { + *dst1 = *entry; + found1 = true; + } else if (!found2 && strcmp(entry->name, name2) == 0) { + *dst2 = *entry; + found2 = true; + } + if (found1 && found2) { + break; + } + } + TMCHECK_UNLOCK(); + return found1 && found2; +} + void osal_tmcheck_print_all(void) { + tmcheck_t snap[OSAL_TMCHECK_NUM_MAX]; int i; - tmcheck_t *tmcheck = NULL; + snapshot_all(snap); for (i = 0; i < OSAL_TMCHECK_NUM_MAX; i++) { - tmcheck = &s_tmcheck_man.tmchecks[i]; - if (tmcheck->ts > 0) { - OSALOG_INFO("[%s]=%" PRIu64 " ns\n", tmcheck->name, tmcheck->ts); + if (snap[i].ts > 0) { + OSALOG_INFO("[%s]=%" PRIu64 " ns\n", snap[i].name, snap[i].ts); } } } @@ -197,10 +250,13 @@ static void tmcheck_print_diff(const tmcheck_t *tmcheck1, const tmcheck_t *tmche void osal_tmcheck_print_diff(int idx1, int idx2) { - if ((validate_idx(idx1) == false) || (validate_idx(idx2) == false)) { + tmcheck_t snap1; + tmcheck_t snap2; + + if (!snapshot_idx(idx1, &snap1) || !snapshot_idx(idx2, &snap2)) { return; } - tmcheck_print_diff(&s_tmcheck_man.tmchecks[idx1], &s_tmcheck_man.tmchecks[idx2]); + tmcheck_print_diff(&snap1, &snap2); } static int tmcheck_cmp(const void *a, const void *b) @@ -219,13 +275,13 @@ static int tmcheck_cmp(const void *a, const void *b) void osal_tmcheck_print_diff_all(void) { - tmcheck_t sort_tmchecks[OSAL_TMCHECK_NUM_MAX] = {0}; + tmcheck_t sort_tmchecks[OSAL_TMCHECK_NUM_MAX]; tmcheck_t *prev_tmcheck = NULL; tmcheck_t *first_tmcheck = NULL; int count = 0; int i; - memcpy(sort_tmchecks, s_tmcheck_man.tmchecks, sizeof(sort_tmchecks)); + snapshot_all(sort_tmchecks); qsort(sort_tmchecks, OSAL_TMCHECK_NUM_MAX, sizeof(tmcheck_t), tmcheck_cmp); for (i = 0; i < OSAL_TMCHECK_NUM_MAX; i++) { @@ -277,52 +333,30 @@ osal_error_t osal_tmcheck_get_diff(int idx1, int idx2, int64_t *out_ns) void osal_tmcheck_name_print_diff(const char *name1, const char *name2) { - int i; - tmcheck_t *tmcheck1 = NULL; - tmcheck_t *tmcheck2 = NULL; + tmcheck_t snap1; + tmcheck_t snap2; - for (i = 0; i < OSAL_TMCHECK_NUM_MAX; i++) { - if (strcmp(s_tmcheck_man.tmchecks[i].name, name1) == 0) { - tmcheck1 = &s_tmcheck_man.tmchecks[i]; - } else if (strcmp(s_tmcheck_man.tmchecks[i].name, name2) == 0) { - tmcheck2 = &s_tmcheck_man.tmchecks[i]; - } - if ((tmcheck1 != NULL) && (tmcheck2 != NULL)) { - break; - } - } - if ((tmcheck1 != NULL) && (tmcheck2 != NULL)) { - /* NOLINTNEXTLINE(readability-suspicious-call-argument): intentional order */ - tmcheck_print_diff(tmcheck2, tmcheck1); - } else { + if ((name1 == NULL) || (name2 == NULL) || !snapshot_names(name1, name2, &snap1, &snap2)) { OSALOG_ERROR("Invalid checkpoint name %s or %s\n", name1, name2); + return; } + /* NOLINTNEXTLINE(readability-suspicious-call-argument): intentional order */ + tmcheck_print_diff(&snap2, &snap1); } osal_error_t osal_tmcheck_name_get_diff(const char *name1, const char *name2, int64_t *out_ns) { - int i; - tmcheck_t *tmcheck1 = NULL; - tmcheck_t *tmcheck2 = NULL; + tmcheck_t snap1; + tmcheck_t snap2; if ((name1 == NULL) || (name2 == NULL) || (out_ns == NULL)) { return OSAL_E_PARAM; } - for (i = 0; i < OSAL_TMCHECK_NUM_MAX; i++) { - if (strcmp(s_tmcheck_man.tmchecks[i].name, name1) == 0) { - tmcheck1 = &s_tmcheck_man.tmchecks[i]; - } else if (strcmp(s_tmcheck_man.tmchecks[i].name, name2) == 0) { - tmcheck2 = &s_tmcheck_man.tmchecks[i]; - } - if ((tmcheck1 != NULL) && (tmcheck2 != NULL)) { - break; - } - } - if ((tmcheck1 == NULL) || (tmcheck2 == NULL)) { + if (!snapshot_names(name1, name2, &snap1, &snap2)) { OSALOG_ERROR("Invalid checkpoint name %s or %s\n", name1, name2); return OSAL_E_PARAM; } - *out_ns = (int64_t)tmcheck2->ts - (int64_t)tmcheck1->ts; + *out_ns = (int64_t)snap2.ts - (int64_t)snap1.ts; return OSAL_E_OK; } @@ -337,18 +371,24 @@ uint64_t osal_tmcheck_get_captured_ts(int idx) uint64_t osal_tmcheck_name_get_captured_ts(const char *name) { int i; - tmcheck_t *tmcheck = NULL; + uint64_t ts = 0; + bool found = false; + if (name == NULL) { + return 0; + } + TMCHECK_LOCK(); for (i = 0; i < OSAL_TMCHECK_NUM_MAX; i++) { if ((s_tmcheck_man.tmchecks[i].inuse != 0) && (strcmp(s_tmcheck_man.tmchecks[i].name, name) == 0)) { - tmcheck = &s_tmcheck_man.tmchecks[i]; + ts = s_tmcheck_man.tmchecks[i].ts; + found = true; break; } } - if (tmcheck != NULL) { - return tmcheck->ts; + TMCHECK_UNLOCK(); + if (!found) { + OSALOG_ERROR("Invalid checkpoint name %s\n", name); } - OSALOG_ERROR("Invalid checkpoint name %s\n", name); - return 0; + return ts; } diff --git a/source/zephyr/osal_queue.c b/source/zephyr/osal_queue.c index ed73ddb..e4e4a4d 100644 --- a/source/zephyr/osal_queue.c +++ b/source/zephyr/osal_queue.c @@ -36,7 +36,7 @@ #include "osal_assert.h" #include "osal_config.h" -/* Shared by every handle created with the same name (mirrors POSIX mq_open attach). */ +/* Shared by every handle created with the same name (mirrors Linux mq_open attach). */ typedef struct { struct k_msgq msgq; char buf[CONFIG_DMOSAL_QUEUE_BUF_SIZE] __aligned(sizeof(void *)); diff --git a/test/osal/tmcheck_test.c b/test/osal/tmcheck_test.c index 6c13959..c9c6533 100644 --- a/test/osal/tmcheck_test.c +++ b/test/osal/tmcheck_test.c @@ -292,15 +292,63 @@ static void tmcheck_mt_task(void *arg) idx = osal_tmcheck_create(name); assert_true(idx >= 0); g_mt_idx[i] = idx; - /* Each task only touches its own checkpoint slot, so this stresses - * the create/delete pool locking concurrently across threads without - * racing on a single tmcheck_t's fields (which are documented as - * caller-serialized / [owner], not internally locked). */ + /* Each task only touches its own checkpoint slot: capture/get are the lock-free + * hot path and are caller-serialized per checkpoint ([owner]). */ osal_tmcheck_capture_ts(idx); ts = osal_tmcheck_get_captured_ts(idx); g_mt_ok[i] = (ts > 0); } +/* Cold-path readers (name lookup, print) racing with create/delete in other threads. */ +static void tmcheck_mt_cold_task(void *arg) +{ + intptr_t i = (intptr_t)arg; + char name1[32]; + char name2[32]; + int round; + + unique_name(name1, sizeof(name1), "cold1", (int)i); + unique_name(name2, sizeof(name2), "cold2", (int)i); + for (round = 0; round < 50; round++) { + int64_t diff_ns; + int idx1 = osal_tmcheck_create(name1); + int idx2 = osal_tmcheck_create(name2); + + if (idx1 >= 0 && idx2 >= 0) { + osal_tmcheck_capture_ts(idx1); + osal_tmcheck_capture_ts(idx2); + (void)osal_tmcheck_name_get_captured_ts(name1); + (void)osal_tmcheck_name_get_diff(name1, name2, &diff_ns); + } + if (idx1 >= 0) { + osal_tmcheck_delete(idx1); + } + if (idx2 >= 0) { + osal_tmcheck_delete(idx2); + } + } +} + +static void test_tmcheck_multithread_cold_path(void **state) +{ + (void)state; + osal_task_t *tasks[TMCHECK_MT_TASKS]; + osal_task_cfg_t cfg; + int i; + + for (i = 0; i < TMCHECK_MT_TASKS; i++) { + memset(&cfg, 0, sizeof(cfg)); + cfg.task_handler = tmcheck_mt_cold_task; + cfg.task_arg = (void *)(intptr_t)i; + tasks[i] = osal_task_create(&cfg); + assert_non_null(tasks[i]); + } + for (i = 0; i < TMCHECK_MT_TASKS; i++) { + osal_task_delete(tasks[i]); /* joins the thread */ + } + assert_int_equal(osal_tmcheck_use(), 0); +} + static void test_tmcheck_multithread_capture(void **state) { (void)state; @@ -572,6 +620,7 @@ int main(void) cmocka_unit_test_setup_teardown(test_tmcheck_name_apis, setup, teardown), cmocka_unit_test_setup_teardown(test_tmcheck_diff_ns_large, setup, teardown), cmocka_unit_test_setup_teardown(test_tmcheck_multithread_capture, setup, teardown), + cmocka_unit_test_setup_teardown(test_tmcheck_multithread_cold_path, setup, teardown), cmocka_unit_test_setup_teardown(test_tmcheck_print_diff, setup, teardown), cmocka_unit_test_setup_teardown(test_tmcheck_print_diff_uncaptured, setup, teardown), cmocka_unit_test_setup_teardown(test_tmcheck_print_all, setup, teardown), diff --git a/zephyr/README.md b/zephyr/README.md index 099ebe3..d7851c1 100644 --- a/zephyr/README.md +++ b/zephyr/README.md @@ -12,8 +12,8 @@ normal Linux process, no real hardware). Only Docker is needed on the host. | `zephyr/app/` | Zephyr app (CMake + `prj.conf` only). Builds a file from `examples/`, shared with all platforms. | | `zephyr/docker.sh`, `zephyr/Dockerfile` | Slim build container + helper. | -The portable code (`source/*.c`) is shared with the POSIX build; do not edit it -for Zephyr. Use `source/posix/*.c` as the reference for behaviour. +The portable code (`source/*.c`) is shared with the Linux build; do not edit it +for Zephyr. Use `source/linux/*.c` as the reference for behaviour. ## Quick start From 660be71349f0320d4c7c98717f930a32e7b1825d Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Sat, 24 Oct 2026 07:04:40 +0900 Subject: [PATCH 3/5] support zephyr unitest --- .github/workflows/ci.yml | 26 +++++++ .gitignore | 1 + CMakeLists.txt | 12 +-- README.md | 4 +- docs/README.md | 2 +- docs/architecture.md | 6 +- docs/lifetime.md | 2 +- docs/porting.md | 18 ++--- docs/thread-safety.md | 3 +- include/dmosal/osal_tmcheck.h | 8 ++ justfile | 19 ++--- source/osal_tmcheck.c | 132 ++++++++++++--------------------- source/zephyr/osal_queue.c | 2 +- source/zephyr/osal_time.c | 4 +- test/CMakeLists.txt | 7 ++ test/linux/queue_unlink_test.c | 82 ++++++++++++++++++++ test/osal/queue_test.c | 26 ------- test/osal/task_test.c | 4 +- test/osal/timer_test.c | 8 +- test/osal/tmcheck_test.c | 75 ++++--------------- zephyr/Dockerfile | 8 +- zephyr/README.md | 11 ++- zephyr/app/CMakeLists.txt | 29 +++++++- zephyr/app/cmocka/config.h | 39 ++++++++++ zephyr/app/test.conf | 2 + zephyr/app/test_main.c | 10 +++ zephyr/docker.sh | 24 +++++- 27 files changed, 341 insertions(+), 223 deletions(-) create mode 100644 test/linux/queue_unlink_test.c create mode 100644 zephyr/app/cmocka/config.h create mode 100644 zephyr/app/test.conf create mode 100644 zephyr/app/test_main.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efee6ca..2bcb659 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,6 +168,32 @@ jobs: files=$(git ls-files 'source/*.c' ':!source/zephyr') clang-tidy -p build $files + zephyr: + name: zephyr native_sim (examples + tests) + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Cache the Zephyr build image + id: image-cache + uses: actions/cache@v4 + with: + path: /tmp/dmosal-zephyr.tar + key: zephyr-image-${{ hashFiles('zephyr/Dockerfile') }} + - name: Load cached image + if: steps.image-cache.outputs.cache-hit == 'true' + run: docker load -i /tmp/dmosal-zephyr.tar + - name: Build the Zephyr image + if: steps.image-cache.outputs.cache-hit != 'true' + run: | + docker build -t dmosal-zephyr:local -f zephyr/Dockerfile zephyr + docker save -o /tmp/dmosal-zephyr.tar dmosal-zephyr:local + - name: Build examples + run: zephyr/docker.sh build + - name: Run examples + run: zephyr/docker.sh run + - name: Build and run cmocka tests + run: zephyr/docker.sh test + docs: name: docs (build) runs-on: ubuntu-22.04 diff --git a/.gitignore b/.gitignore index cc6cc41..61d235b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build GPATH GRTAGS GTAGS +build-*/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 542db1b..65e4880 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,12 +78,12 @@ set(osal_SRC source/osal_log.c source/osal_rm.c source/osal_tmcheck.c - source/linux/osal_mutex.c - source/linux/osal_queue.c - source/linux/osal_sem.c - source/linux/osal_task.c - source/linux/osal_time.c - source/linux/osal_timer.c + source/posix/osal_mutex.c + source/posix/osal_queue.c + source/posix/osal_sem.c + source/posix/osal_task.c + source/posix/osal_time.c + source/posix/osal_timer.c ) # Generated headers live under the build tree — never in the source tree. diff --git a/README.md b/README.md index d03ad04..c63bfcd 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ semantics across POSIX and RTOS platforms. Everything (tasks, mutexes, semaphores, queues, timers, ...) is allocated statically up front through a central resource manager, so there's no malloc on the hot path and the worst-case memory usage is known ahead of time. A -Linux backend and a Zephyr backend are included; other targets can be +POSIX/Linux backend and a Zephyr backend are included; other targets can be added behind the same API without breaking existing callers. ``` @@ -87,7 +87,7 @@ backend maps to Zephyr primitives. ## Examples Sample programs live in [examples/](examples/). Build them with `just build` -(CMake target `examples`) for Linux, or with `zephyr/docker.sh build` for +(CMake target `examples`) for POSIX, or with `zephyr/docker.sh build` for Zephyr. ## Documentation diff --git a/docs/README.md b/docs/README.md index 2479012..0293d82 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ Doxygen-generated API reference (built via `make doc`). | [architecture.md](architecture.md) | Layered design, subsystem overview, resource-manager pattern, callback threading model, backend model. | | [api-spec.md](api-spec.md) | Behavioral specification per subsystem — functions, states, error codes, invariants. | | [lifetime.md](lifetime.md) | Init/deinit ordering, resource ownership, when it is safe to use / release a resource. | -| [porting.md](porting.md) | How to add a new backend (e.g. an RTOS or bare-metal target). Reference: the Linux backend. | +| [porting.md](porting.md) | How to add a new backend (e.g. an RTOS or bare-metal target). Reference: the POSIX backend. | | [thread-safety.md](thread-safety.md) | Per-API table classifying every public call as thread-safe, single-owner, or init-thread-only. | If you're just getting started, read them in this order: diff --git a/docs/architecture.md b/docs/architecture.md index 0edd6cf..dbc2852 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,7 +18,7 @@ behaviour matters more than dynamic flexibility. | dmosal (public API) | +---------------+---------------+ | portable | backend | - | layer | (Linux...) | + | layer | (POSIX...) | +---------------+---------------+ | OS user-space API | +-------------------------------+ @@ -34,7 +34,7 @@ behaviour matters more than dynamic flexibility. across every backend (resource manager, LIFO free-list, error strings, logging, time-checkpoint utility, top-level init). - **Backend** — `source//*.c`. Only these files touch OS-specific - APIs. The Linux backend under `source/linux/` is the reference + APIs. The POSIX backend under `source/posix/` is the reference implementation. ## Subsystems @@ -93,7 +93,7 @@ Two subsystems invoke user code from a thread other than the caller's: - **Task** — `osal_task_create(cfg)` spawns a new thread that runs `cfg->task_handler`. The handler runs to completion on that thread. - **Timer** — the timer callback fires on a backend-managed context, not - the caller's thread. On the Linux backend that means a fresh helper + the caller's thread. On the POSIX backend that means a fresh helper thread per expiration (via `SIGEV_THREAD`). User code must not assume any particular thread identity inside the callback. diff --git a/docs/lifetime.md b/docs/lifetime.md index 18d87bf..0b5937f 100644 --- a/docs/lifetime.md +++ b/docs/lifetime.md @@ -133,7 +133,7 @@ create -> [started/stopped, callback fires 0+ times] -> stop -> delete create --opens or attaches--> [send/recv N times] --unlinks if creator--> delete ``` -- On the Linux backend, `osal_queue_create` may either create the +- On the POSIX backend, `osal_queue_create` may either create the underlying kernel queue or attach to an existing one with the same name. `osal_queue_delete` unlinks the kernel queue only when this handle originally created it. diff --git a/docs/porting.md b/docs/porting.md index 5d3c1e3..e3608a4 100644 --- a/docs/porting.md +++ b/docs/porting.md @@ -5,7 +5,7 @@ under `source/`. The rest of the library — the resource manager, LIFO, logging, error strings, tmcheck, and top-level init — is portable C and unchanged across backends. -The reference implementation lives at `source/linux/` and targets Linux / +The reference implementation lives at `source/posix/` and targets Linux / glibc. This document walks through what a new backend must provide. ## Files a backend must implement @@ -31,7 +31,7 @@ For each function, honour the behavior documented in [lifetime.md](lifetime.md). Concretely: - **Pool ops must be thread-safe** — `_create` and `_delete` need to - serialize their pool touches. On Linux we do this by threading the + serialize their pool touches. On POSIX we do this by threading the shared `osal_mutex_t *mutex` through each `_man_t`'s `osal_rm_t`. Reuse that pattern via `OSAL_RM_USEROBJMAN_DECLARE` / `_INIT` if the target OS has a mutex primitive. @@ -43,7 +43,7 @@ For each function, honour the behavior documented in without a helper-thread mechanism, provide one (e.g. a dedicated timer dispatch task). - **`osal_timer_delete` must reject in-callback invocation.** Use the - `in_callback` bool pattern from the Linux backend: set true around the + `in_callback` bool pattern from the POSIX backend: set true around the callback invocation, `OSAL_RUNTIME_ASSERT` in delete if it's true. ## Steps @@ -61,13 +61,13 @@ For each function, honour the behavior documented in 2. **Add sources to `CMakeLists.txt`.** Extend the `osal_SRC` list to include `source/mybackend/*.c` when the target backend is selected. - The simplest scheme: add an option `DMOSAL_BACKEND=linux|mybackend` + The simplest scheme: add an option `DMOSAL_BACKEND=posix|mybackend` and switch the source glob accordingly. Only one backend is compiled in at a time. 3. **Implement `osal_mutex.c` first.** Everything else depends on it — the shared mutex from this pool is passed to every other subsystem's - init. See `source/linux/osal_mutex.c` for the reference: + init. See `source/posix/osal_mutex.c` for the reference: - `s_mutex_man` static holding the pool. - A bare backend-level mutex (`s_mutex_man.resrc_mutex`) is used inside `osal_mutex_create` because it can't recursively use its own @@ -77,11 +77,11 @@ For each function, honour the behavior documented in 4. **Implement `osal_sem.c`, `osal_task.c`, `osal_timer.c`, `osal_queue.c`, `osal_time.c`.** Each mirrors the same `_man_t` + `osal_rm_t` structure. - Copy the Linux file and swap the OS calls. + Copy the POSIX file and swap the OS calls. 5. **Wire in the build.** `CMakeLists.txt` is the only place where the backend directory is referenced. Neither the public headers nor the - portable `source/*.c` files hard-code Linux. + portable `source/*.c` files hard-code POSIX. 6. **Run the existing test suite.** All six cmocka tests exercise only the public API and should pass unchanged. If they fail, the failure @@ -96,8 +96,8 @@ Some behaviors are inherently backend-specific and may need adjustment: the microsecond field will just always show 0 — no code change needed. - **`osal_task_cfg_t` hints** — `stack_addr`, `stack_size`, `priority`, `name` are best-effort. Honour them if your target OS supports them. - The Linux reference implementation ignores them. -- **Timer callback thread** — the Linux backend spawns one helper thread + The POSIX reference implementation ignores them. +- **Timer callback thread** — the POSIX backend spawns one helper thread per expiration (`SIGEV_THREAD`). On memory-constrained targets, use a single dedicated timer-dispatch task that walks a pending list. diff --git a/docs/thread-safety.md b/docs/thread-safety.md index ecd19fd..682bf86 100644 --- a/docs/thread-safety.md +++ b/docs/thread-safety.md @@ -123,8 +123,7 @@ generally safe on most systems. |---|---| | `osal_tmcheck_init`, `osal_tmcheck_deinit` | **[init]** | | `osal_tmcheck_create`, `osal_tmcheck_delete` | **[any]** — internally locked | -| all `osal_tmcheck_*_print*`, `osal_tmcheck_name_get_diff`, `osal_tmcheck_name_get_captured_ts` | **[any]** — cold path, internally locked (names and the in-use flag are read under the shared mutex) | -| `osal_tmcheck_capture_ts`, `osal_tmcheck_reset`, `osal_tmcheck_reset_all`, `osal_tmcheck_get_diff`, `osal_tmcheck_get_captured_ts` | **[owner]** — hot path, intentionally lock-free so measurement is not perturbed; the caller serializes access to a given checkpoint. Data may be slightly inconsistent if racing against `_delete` | +| `osal_tmcheck_capture_ts`, `osal_tmcheck_reset`, `osal_tmcheck_reset_all`, all `osal_tmcheck_*_print*`, `osal_tmcheck_get_diff`, `osal_tmcheck_name_get_diff`, `osal_tmcheck_get_captured_ts`, `osal_tmcheck_name_get_captured_ts` | **[owner]** — diagnostic-only paths intentionally unlocked. Data may be slightly inconsistent if racing against `_delete` | | `osal_tmcheck_use`, `osal_tmcheck_avail` | **[any]** | ## Assert diff --git a/include/dmosal/osal_tmcheck.h b/include/dmosal/osal_tmcheck.h index 4578b98..9489fe4 100644 --- a/include/dmosal/osal_tmcheck.h +++ b/include/dmosal/osal_tmcheck.h @@ -66,6 +66,14 @@ extern "C" { * * A static checkpoint is created (if not already allocated) based on the function * name and line number, and the current timestamp is captured for it. + * + * @warning The checkpoint is a per-call-site static, so the same code in the same + * function must not be run from multiple threads through this macro: all threads would + * share one checkpoint (capture is one-shot, so only the first timestamp is kept), and + * the lazy creation is not thread-safe. In that case, allocate a unique id per thread, + * create one checkpoint per id with osal_tmcheck_create() (for example named + * "func:line:id"), and call osal_tmcheck_capture_ts() with that index instead of + * using this macro. */ #define OSAL_TMCHECK_CAPTURE() \ do { \ diff --git a/justfile b/justfile index f836cad..0054a50 100644 --- a/justfile +++ b/justfile @@ -18,6 +18,7 @@ # just tidy # run clang-tidy # just zephyr-build # build the examples for Zephyr native_sim (Docker) # just zephyr-run # run the Zephyr examples +# just zephyr-test # run the cmocka tests on Zephyr native_sim (TEST= for one) # just zephyr-shell # shell inside the Zephyr container # just clean # remove build directories @@ -76,7 +77,7 @@ doc: configure format: pre-commit run clang-format --all-files -# Run clang-tidy over the portable + Linux sources via pre-commit +# Run clang-tidy over the portable + POSIX sources via pre-commit tidy: cmake -S . -B {{build_dir}} -DCMAKE_BUILD_TYPE={{build_type}} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON cmake --build {{build_dir}} --target dmosal -j{{jobs}} @@ -90,17 +91,9 @@ zephyr-build: zephyr-run: zephyr/docker.sh run -# Open a shell in the Zephyr container -zephyr-shell: - zephyr/docker.sh shell - -# Build the examples for Zephyr native_sim inside Docker (EXAMPLE= for one) -zephyr-build: - zephyr/docker.sh build - -# Run the Zephyr examples on native_sim -zephyr-run: - zephyr/docker.sh run +# Build and run the cmocka tests (test/osal) on Zephyr native_sim +zephyr-test: + zephyr/docker.sh test # Open a shell in the Zephyr container zephyr-shell: @@ -112,4 +105,4 @@ clean: # Remove all build directories (default + sanitizer variants) distclean: - rm -rf {{build_dir}} build-asan build-tsan build-zephyr + rm -rf {{build_dir}} build-asan build-tsan build-zephyr build-zephyr-test diff --git a/source/osal_tmcheck.c b/source/osal_tmcheck.c index f75718d..82cc653 100644 --- a/source/osal_tmcheck.c +++ b/source/osal_tmcheck.c @@ -165,68 +165,15 @@ void osal_tmcheck_capture_ts(int idx) } } -/* - * Cold-path functions (print and name lookup) take the lock because they read - * names / the inuse flag that create and delete modify. capture_ts, get_captured_ts, - * reset, reset_all and get_diff are the hot path and stay lock-free: the caller serializes - * access to a given checkpoint. - */ -static void snapshot_all(tmcheck_t *dst) -{ - TMCHECK_LOCK(); - memcpy(dst, s_tmcheck_man.tmchecks, sizeof(s_tmcheck_man.tmchecks)); - TMCHECK_UNLOCK(); -} - -static bool snapshot_idx(int idx, tmcheck_t *dst) -{ - if (validate_idx(idx) == false) { - return false; - } - TMCHECK_LOCK(); - *dst = s_tmcheck_man.tmchecks[idx]; - TMCHECK_UNLOCK(); - return true; -} - -/* Look up two names under the lock; returns true when both are found. */ -static bool snapshot_names(const char *name1, const char *name2, tmcheck_t *dst1, tmcheck_t *dst2) -{ - bool found1 = false; - bool found2 = false; - int i; - - TMCHECK_LOCK(); - for (i = 0; i < OSAL_TMCHECK_NUM_MAX; i++) { - const tmcheck_t *entry = &s_tmcheck_man.tmchecks[i]; - - if (entry->inuse == 0) { - continue; - } - if (!found1 && strcmp(entry->name, name1) == 0) { - *dst1 = *entry; - found1 = true; - } else if (!found2 && strcmp(entry->name, name2) == 0) { - *dst2 = *entry; - found2 = true; - } - if (found1 && found2) { - break; - } - } - TMCHECK_UNLOCK(); - return found1 && found2; -} - void osal_tmcheck_print_all(void) { - tmcheck_t snap[OSAL_TMCHECK_NUM_MAX]; int i; + tmcheck_t *tmcheck = NULL; - snapshot_all(snap); for (i = 0; i < OSAL_TMCHECK_NUM_MAX; i++) { - if (snap[i].ts > 0) { - OSALOG_INFO("[%s]=%" PRIu64 " ns\n", snap[i].name, snap[i].ts); + tmcheck = &s_tmcheck_man.tmchecks[i]; + if (tmcheck->ts > 0) { + OSALOG_INFO("[%s]=%" PRIu64 " ns\n", tmcheck->name, tmcheck->ts); } } } @@ -250,13 +197,10 @@ static void tmcheck_print_diff(const tmcheck_t *tmcheck1, const tmcheck_t *tmche void osal_tmcheck_print_diff(int idx1, int idx2) { - tmcheck_t snap1; - tmcheck_t snap2; - - if (!snapshot_idx(idx1, &snap1) || !snapshot_idx(idx2, &snap2)) { + if ((validate_idx(idx1) == false) || (validate_idx(idx2) == false)) { return; } - tmcheck_print_diff(&snap1, &snap2); + tmcheck_print_diff(&s_tmcheck_man.tmchecks[idx1], &s_tmcheck_man.tmchecks[idx2]); } static int tmcheck_cmp(const void *a, const void *b) @@ -275,13 +219,13 @@ static int tmcheck_cmp(const void *a, const void *b) void osal_tmcheck_print_diff_all(void) { - tmcheck_t sort_tmchecks[OSAL_TMCHECK_NUM_MAX]; + tmcheck_t sort_tmchecks[OSAL_TMCHECK_NUM_MAX] = {0}; tmcheck_t *prev_tmcheck = NULL; tmcheck_t *first_tmcheck = NULL; int count = 0; int i; - snapshot_all(sort_tmchecks); + memcpy(sort_tmchecks, s_tmcheck_man.tmchecks, sizeof(sort_tmchecks)); qsort(sort_tmchecks, OSAL_TMCHECK_NUM_MAX, sizeof(tmcheck_t), tmcheck_cmp); for (i = 0; i < OSAL_TMCHECK_NUM_MAX; i++) { @@ -333,30 +277,52 @@ osal_error_t osal_tmcheck_get_diff(int idx1, int idx2, int64_t *out_ns) void osal_tmcheck_name_print_diff(const char *name1, const char *name2) { - tmcheck_t snap1; - tmcheck_t snap2; + int i; + tmcheck_t *tmcheck1 = NULL; + tmcheck_t *tmcheck2 = NULL; - if ((name1 == NULL) || (name2 == NULL) || !snapshot_names(name1, name2, &snap1, &snap2)) { + for (i = 0; i < OSAL_TMCHECK_NUM_MAX; i++) { + if (strcmp(s_tmcheck_man.tmchecks[i].name, name1) == 0) { + tmcheck1 = &s_tmcheck_man.tmchecks[i]; + } else if (strcmp(s_tmcheck_man.tmchecks[i].name, name2) == 0) { + tmcheck2 = &s_tmcheck_man.tmchecks[i]; + } + if ((tmcheck1 != NULL) && (tmcheck2 != NULL)) { + break; + } + } + if ((tmcheck1 != NULL) && (tmcheck2 != NULL)) { + /* NOLINTNEXTLINE(readability-suspicious-call-argument): intentional order */ + tmcheck_print_diff(tmcheck2, tmcheck1); + } else { OSALOG_ERROR("Invalid checkpoint name %s or %s\n", name1, name2); - return; } - /* NOLINTNEXTLINE(readability-suspicious-call-argument): intentional order */ - tmcheck_print_diff(&snap2, &snap1); } osal_error_t osal_tmcheck_name_get_diff(const char *name1, const char *name2, int64_t *out_ns) { - tmcheck_t snap1; - tmcheck_t snap2; + int i; + tmcheck_t *tmcheck1 = NULL; + tmcheck_t *tmcheck2 = NULL; if ((name1 == NULL) || (name2 == NULL) || (out_ns == NULL)) { return OSAL_E_PARAM; } - if (!snapshot_names(name1, name2, &snap1, &snap2)) { + for (i = 0; i < OSAL_TMCHECK_NUM_MAX; i++) { + if (strcmp(s_tmcheck_man.tmchecks[i].name, name1) == 0) { + tmcheck1 = &s_tmcheck_man.tmchecks[i]; + } else if (strcmp(s_tmcheck_man.tmchecks[i].name, name2) == 0) { + tmcheck2 = &s_tmcheck_man.tmchecks[i]; + } + if ((tmcheck1 != NULL) && (tmcheck2 != NULL)) { + break; + } + } + if ((tmcheck1 == NULL) || (tmcheck2 == NULL)) { OSALOG_ERROR("Invalid checkpoint name %s or %s\n", name1, name2); return OSAL_E_PARAM; } - *out_ns = (int64_t)snap2.ts - (int64_t)snap1.ts; + *out_ns = (int64_t)tmcheck2->ts - (int64_t)tmcheck1->ts; return OSAL_E_OK; } @@ -371,24 +337,18 @@ uint64_t osal_tmcheck_get_captured_ts(int idx) uint64_t osal_tmcheck_name_get_captured_ts(const char *name) { int i; - uint64_t ts = 0; - bool found = false; + tmcheck_t *tmcheck = NULL; - if (name == NULL) { - return 0; - } - TMCHECK_LOCK(); for (i = 0; i < OSAL_TMCHECK_NUM_MAX; i++) { if ((s_tmcheck_man.tmchecks[i].inuse != 0) && (strcmp(s_tmcheck_man.tmchecks[i].name, name) == 0)) { - ts = s_tmcheck_man.tmchecks[i].ts; - found = true; + tmcheck = &s_tmcheck_man.tmchecks[i]; break; } } - TMCHECK_UNLOCK(); - if (!found) { - OSALOG_ERROR("Invalid checkpoint name %s\n", name); + if (tmcheck != NULL) { + return tmcheck->ts; } - return ts; + OSALOG_ERROR("Invalid checkpoint name %s\n", name); + return 0; } diff --git a/source/zephyr/osal_queue.c b/source/zephyr/osal_queue.c index e4e4a4d..ed73ddb 100644 --- a/source/zephyr/osal_queue.c +++ b/source/zephyr/osal_queue.c @@ -36,7 +36,7 @@ #include "osal_assert.h" #include "osal_config.h" -/* Shared by every handle created with the same name (mirrors Linux mq_open attach). */ +/* Shared by every handle created with the same name (mirrors POSIX mq_open attach). */ typedef struct { struct k_msgq msgq; char buf[CONFIG_DMOSAL_QUEUE_BUF_SIZE] __aligned(sizeof(void *)); diff --git a/source/zephyr/osal_time.c b/source/zephyr/osal_time.c index 9a6dc59..d66c04a 100644 --- a/source/zephyr/osal_time.c +++ b/source/zephyr/osal_time.c @@ -48,6 +48,8 @@ osal_error_t osal_clock_time(uint64_t *nsec) return OSAL_E_PARAM; } - *nsec = k_ticks_to_ns_floor64(k_uptime_ticks()); + /* Uptime is 0 right after boot (always so on native_sim before time advances); callers + * such as tmcheck use 0 as "not set", so never return it. */ + *nsec = k_ticks_to_ns_floor64(k_uptime_ticks()) + 1U; return OSAL_E_OK; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a56c1d0..6afca12 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -79,3 +79,10 @@ add_executable(${OSAL_TEST} osal/osal_test.c) target_link_libraries(${OSAL_TEST} dmosal ${CMOCKA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) add_dependencies(check ${OSAL_TEST}) add_test(${OSAL_TEST} ${OSAL_TEST}) + +# Linux-backend-only tests (not portable to other backends). +set(QUEUE_UNLINK_TEST queue_unlink_test) +add_executable(${QUEUE_UNLINK_TEST} linux/queue_unlink_test.c) +target_link_libraries(${QUEUE_UNLINK_TEST} dmosal ${CMOCKA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) +add_dependencies(check ${QUEUE_UNLINK_TEST}) +add_test(${QUEUE_UNLINK_TEST} ${QUEUE_UNLINK_TEST}) diff --git a/test/linux/queue_unlink_test.c b/test/linux/queue_unlink_test.c new file mode 100644 index 0000000..a2268e9 --- /dev/null +++ b/test/linux/queue_unlink_test.c @@ -0,0 +1,82 @@ +/* BSD 2-Clause License +* +* Copyright (c) 2023, nguyenvannam142@gmail.com +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, this +* list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* Linux-only: the Linux queue backend is backed by a POSIX message queue, visible in + * /dev/mqueue. Other backends have no such kernel object. */ +#include +#include "cmocka_include.h" +#include "osal.h" + +static void test_queue_delete_unlinks(void **state) +{ + (void)state; + osal_queue_cfg_t cfg; + osal_queue_t *queue; + char path[256]; + + memset(&cfg, 0, sizeof(cfg)); + snprintf((char *)cfg.name, sizeof(cfg.name), "dmosal_qu_%d", (int)getpid()); + cfg.msglen = 8; + cfg.qsize = 2; + + queue = osal_queue_create(&cfg); + assert_non_null(queue); + + snprintf(path, sizeof(path), "/dev/mqueue/%s", (char *)cfg.name); + assert_int_equal(access(path, F_OK), 0); + + osal_queue_delete(queue); + + assert_int_not_equal(access(path, F_OK), 0); + assert_int_equal(errno, ENOENT); +} + +static int setup(void **state) +{ + osal_config_t cfg; + + (void)state; + memset(&cfg, 0, sizeof(cfg)); + osal_init(&cfg); + return 0; +} + +static int teardown(void **state) +{ + (void)state; + osal_deinit(); + return 0; +} + +int main(void) +{ + setenv("CMOCKA_TEST_ABORT", "1", 1); + + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(test_queue_delete_unlinks, setup, teardown), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/test/osal/queue_test.c b/test/osal/queue_test.c index 640e5be..bf0420c 100644 --- a/test/osal/queue_test.c +++ b/test/osal/queue_test.c @@ -24,7 +24,6 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include #include "cmocka_include.h" #include "osal.h" @@ -224,30 +223,6 @@ static void test_queue_reopen_mismatch(void **state) osal_queue_delete(creator); } -static void test_queue_delete_unlinks(void **state) -{ - (void)state; - osal_queue_cfg_t cfg; - osal_queue_t *queue; - char path[256]; - - memset(&cfg, 0, sizeof(cfg)); - unique_name((char *)cfg.name, sizeof(cfg.name), "unlink", 0); - cfg.msglen = 8; - cfg.qsize = 2; - - queue = osal_queue_create(&cfg); - assert_non_null(queue); - - snprintf(path, sizeof(path), "/dev/mqueue/%s", (char *)cfg.name); - assert_int_equal(access(path, F_OK), 0); - - osal_queue_delete(queue); - - assert_int_not_equal(access(path, F_OK), 0); - assert_int_equal(errno, ENOENT); -} - static void test_queue_pool_exhaustion(void **state) { (void)state; @@ -361,7 +336,6 @@ int main(void) cmocka_unit_test_setup_teardown(test_queue_recv_timeout, setup, teardown), cmocka_unit_test_setup_teardown(test_queue_reopen_match, setup, teardown), cmocka_unit_test_setup_teardown(test_queue_reopen_mismatch, setup, teardown), - cmocka_unit_test_setup_teardown(test_queue_delete_unlinks, setup, teardown), cmocka_unit_test_setup_teardown(test_queue_pool_exhaustion, setup, teardown), cmocka_unit_test_setup_teardown(test_queue_send_recv_invalid_params, setup, teardown), cmocka_unit_test_setup_teardown(test_queue_delete_null, setup, teardown), diff --git a/test/osal/task_test.c b/test/osal/task_test.c index 684810f..59932d2 100644 --- a/test/osal/task_test.c +++ b/test/osal/task_test.c @@ -93,7 +93,7 @@ static void test_task_create(void **state) tasks[i] = osal_task_create(&cfg); assert_non_null(tasks[i]); - usleep(1000); + osal_usleep(1000); } task = osal_task_create(&cfg); assert_null(task); @@ -127,7 +127,7 @@ static void test_task_delete(void **state) task = osal_task_create(&cfg); assert_non_null(task); - usleep(1000); + osal_usleep(1000); osal_task_delete(task); } task = osal_task_create(&cfg); diff --git a/test/osal/timer_test.c b/test/osal/timer_test.c index 4b4e0a0..8d8db14 100644 --- a/test/osal/timer_test.c +++ b/test/osal/timer_test.c @@ -112,7 +112,7 @@ static void test_timer_oneshot(void **state) assert_int_equal(get_timer_count(), 1); /* One-shot: confirm it really doesn't repeat. */ - usleep(50000); + osal_usleep(50000); assert_int_equal(get_timer_count(), 1); osal_timer_delete(timer); @@ -141,9 +141,9 @@ static void test_timer_periodic(void **state) osal_timer_stop(timer); /* Let any expiration already queued right before stop drain out. */ - usleep(50000); + osal_usleep(50000); stopped_count = get_timer_count(); - usleep(50000); + osal_usleep(50000); assert_int_equal(get_timer_count(), stopped_count); osal_timer_delete(timer); @@ -164,7 +164,7 @@ static void test_timer_stop_before_fire(void **state) assert_int_equal(res, OSAL_E_OK); osal_timer_stop(timer); - usleep(50000); + osal_usleep(50000); assert_int_equal(get_timer_count(), 0); osal_timer_delete(timer); diff --git a/test/osal/tmcheck_test.c b/test/osal/tmcheck_test.c index c9c6533..34667fa 100644 --- a/test/osal/tmcheck_test.c +++ b/test/osal/tmcheck_test.c @@ -139,7 +139,7 @@ static void test_tmcheck_capture_first_hit_only(void **state) ts1 = osal_tmcheck_get_captured_ts(idx); assert_true(ts1 > 0); - usleep(20000); + osal_usleep(20000); /* Second capture on the same checkpoint is a no-op until reset. */ osal_tmcheck_capture_ts(idx); ts2 = osal_tmcheck_get_captured_ts(idx); @@ -220,7 +220,7 @@ static void test_tmcheck_name_apis(void **state) assert_true(idx2 >= 0); osal_tmcheck_capture_ts(idx1); - usleep(20000); + osal_usleep(20000); osal_tmcheck_capture_ts(idx2); res = osal_tmcheck_name_get_diff(name1, name2, &out_ns); @@ -292,63 +292,15 @@ static void tmcheck_mt_task(void *arg) idx = osal_tmcheck_create(name); assert_true(idx >= 0); g_mt_idx[i] = idx; - /* Each task only touches its own checkpoint slot: capture/get are the lock-free - * hot path and are caller-serialized per checkpoint ([owner]). */ + /* Each task only touches its own checkpoint slot, so this stresses + * the create/delete pool locking concurrently across threads without + * racing on a single tmcheck_t's fields (which are documented as + * caller-serialized / [owner], not internally locked). */ osal_tmcheck_capture_ts(idx); ts = osal_tmcheck_get_captured_ts(idx); g_mt_ok[i] = (ts > 0); } -/* Cold-path readers (name lookup, print) racing with create/delete in other threads. */ -static void tmcheck_mt_cold_task(void *arg) -{ - intptr_t i = (intptr_t)arg; - char name1[32]; - char name2[32]; - int round; - - unique_name(name1, sizeof(name1), "cold1", (int)i); - unique_name(name2, sizeof(name2), "cold2", (int)i); - for (round = 0; round < 50; round++) { - int64_t diff_ns; - int idx1 = osal_tmcheck_create(name1); - int idx2 = osal_tmcheck_create(name2); - - if (idx1 >= 0 && idx2 >= 0) { - osal_tmcheck_capture_ts(idx1); - osal_tmcheck_capture_ts(idx2); - (void)osal_tmcheck_name_get_captured_ts(name1); - (void)osal_tmcheck_name_get_diff(name1, name2, &diff_ns); - } - if (idx1 >= 0) { - osal_tmcheck_delete(idx1); - } - if (idx2 >= 0) { - osal_tmcheck_delete(idx2); - } - } -} - -static void test_tmcheck_multithread_cold_path(void **state) -{ - (void)state; - osal_task_t *tasks[TMCHECK_MT_TASKS]; - osal_task_cfg_t cfg; - int i; - - for (i = 0; i < TMCHECK_MT_TASKS; i++) { - memset(&cfg, 0, sizeof(cfg)); - cfg.task_handler = tmcheck_mt_cold_task; - cfg.task_arg = (void *)(intptr_t)i; - tasks[i] = osal_task_create(&cfg); - assert_non_null(tasks[i]); - } - for (i = 0; i < TMCHECK_MT_TASKS; i++) { - osal_task_delete(tasks[i]); /* joins the thread */ - } - assert_int_equal(osal_tmcheck_use(), 0); -} - static void test_tmcheck_multithread_capture(void **state) { (void)state; @@ -367,7 +319,11 @@ static void test_tmcheck_multithread_capture(void **state) assert_non_null(tasks[i]); } for (i = 0; i < TMCHECK_MT_TASKS; i++) { - osal_task_delete(tasks[i]); /* joins the thread */ + /* Backends differ on whether delete waits for the task, so wait explicitly. */ + for (int wait = 0; wait < 1000 && !g_mt_ok[i]; wait++) { + osal_usleep(1000); + } + osal_task_delete(tasks[i]); } for (i = 0; i < TMCHECK_MT_TASKS; i++) { @@ -393,7 +349,7 @@ static void test_tmcheck_print_diff(void **state) assert_true(idx2 >= 0); osal_tmcheck_capture_ts(idx1); - usleep(20000); + osal_usleep(20000); osal_tmcheck_capture_ts(idx2); reset_capture(); @@ -483,9 +439,9 @@ static void test_tmcheck_print_diff_all(void **state) assert_true(idx3 >= 0); osal_tmcheck_capture_ts(idx1); - usleep(10000); + osal_usleep(10000); osal_tmcheck_capture_ts(idx2); - usleep(10000); + osal_usleep(10000); osal_tmcheck_capture_ts(idx3); /* With 3 captured checkpoints, print_diff_all prints consecutive @@ -546,7 +502,7 @@ static void test_tmcheck_name_print_diff(void **state) assert_true(idx2 >= 0); osal_tmcheck_capture_ts(idx1); - usleep(20000); + osal_usleep(20000); osal_tmcheck_capture_ts(idx2); reset_capture(); @@ -620,7 +576,6 @@ int main(void) cmocka_unit_test_setup_teardown(test_tmcheck_name_apis, setup, teardown), cmocka_unit_test_setup_teardown(test_tmcheck_diff_ns_large, setup, teardown), cmocka_unit_test_setup_teardown(test_tmcheck_multithread_capture, setup, teardown), - cmocka_unit_test_setup_teardown(test_tmcheck_multithread_cold_path, setup, teardown), cmocka_unit_test_setup_teardown(test_tmcheck_print_diff, setup, teardown), cmocka_unit_test_setup_teardown(test_tmcheck_print_diff_uncaptured, setup, teardown), cmocka_unit_test_setup_teardown(test_tmcheck_print_all, setup, teardown), diff --git a/zephyr/Dockerfile b/zephyr/Dockerfile index e66bc42..2d9a50f 100644 --- a/zephyr/Dockerfile +++ b/zephyr/Dockerfile @@ -13,7 +13,7 @@ ENV DEBIAN_FRONTEND=noninteractive \ RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential ca-certificates cmake device-tree-compiler git gperf \ - ninja-build python3 python3-venv \ + ninja-build python3 python3-venv curl xz-utils \ && rm -rf /var/lib/apt/lists/* \ && python3 -m venv /opt/venv \ && pip install --no-cache-dir west @@ -27,6 +27,12 @@ RUN west init --mr ${ZEPHYR_VERSION} /opt/zephyrws \ && pip install --no-cache-dir -r scripts/requirements-base.txt \ && chmod -R a+rX /opt/zephyrws +# cmocka (Zephyr 4.0 ships no cmocka module): the tests in test/osal are built against its +# sources by zephyr/app. +ARG CMOCKA_VERSION=1.1.7 +RUN mkdir -p /opt/cmocka \ + && curl -fsSL https://cmocka.org/files/1.1/cmocka-${CMOCKA_VERSION}.tar.xz \ + | tar -xJ -C /opt/cmocka --strip-components=1 # west and cmake write caches inside the Zephyr tree, and the container runs as the host user. RUN mkdir -p /opt/zephyrws/zephyr/.cache && chmod -R a+rwX /opt/zephyrws/.west /opt/zephyrws/zephyr/.cache diff --git a/zephyr/README.md b/zephyr/README.md index d7851c1..25bd730 100644 --- a/zephyr/README.md +++ b/zephyr/README.md @@ -12,8 +12,8 @@ normal Linux process, no real hardware). Only Docker is needed on the host. | `zephyr/app/` | Zephyr app (CMake + `prj.conf` only). Builds a file from `examples/`, shared with all platforms. | | `zephyr/docker.sh`, `zephyr/Dockerfile` | Slim build container + helper. | -The portable code (`source/*.c`) is shared with the Linux build; do not edit it -for Zephyr. Use `source/linux/*.c` as the reference for behaviour. +The portable code (`source/*.c`) is shared with the POSIX build; do not edit it +for Zephyr. Use `source/posix/*.c` as the reference for behaviour. ## Quick start @@ -22,6 +22,8 @@ zephyr/docker.sh build # first run builds the image (~1.8 GB, Zephyr v4.0.0 in zephyr/docker.sh run # run every example zephyr/docker.sh shell # interactive shell (west, cmake, gcc available) EXAMPLE=task_sample zephyr/docker.sh build # just one example; same for run +zephyr/docker.sh test # build + run every test/osal/*_test.c on native_sim +TEST=mutex_test zephyr/docker.sh test # just one test ``` `run` passes `-stop_at=3` (3 s of simulated time) because `zephyr.exe` never exits on its own. @@ -52,5 +54,10 @@ Add new examples under `examples/` (portable code only) and select them with `EX - The app finds the module via `ZEPHYR_EXTRA_MODULES` in `zephyr/app/CMakeLists.txt`. Your own app does the same and sets `CONFIG_DMOSAL=y`. - Zephyr source: inside the image at `/opt/zephyrws/zephyr` (browse with `zephyr/docker.sh shell`). +- Tests: `test/osal/*_test.c` are portable cmocka tests shared with the Linux build; they use + only the OSAL API. Backend-specific tests live in `test//` (for example + `test/linux/`) and are not built for Zephyr. `zephyr/app` builds one test with + `-DTEST=`, compiling cmocka from the image (`/opt/cmocka`) against the host libc + (`zephyr/app/test.conf`); `test_main.c` runs the test's `main` and exits with its result. - Only one `main()` per Zephyr image; run extra "apps" as OSAL tasks. - Log level, `osal_log_*` etc. come from the portable code and print via stdout. diff --git a/zephyr/app/CMakeLists.txt b/zephyr/app/CMakeLists.txt index da5ffe8..17d85e1 100644 --- a/zephyr/app/CMakeLists.txt +++ b/zephyr/app/CMakeLists.txt @@ -3,10 +3,35 @@ cmake_minimum_required(VERSION 3.20.0) # Pick up the dmosal Zephyr module from this repository without a west manifest. list(APPEND ZEPHYR_EXTRA_MODULES "${CMAKE_CURRENT_LIST_DIR}/../..") -# Which file in examples/ to build: west build ... -- -DEXAMPLE=task_sample +# Build one file from examples/ ... west build ... -- -DEXAMPLE=task_sample +# ... or one cmocka test from test/osal/: west build ... -- -DTEST=mutex_test set(EXAMPLE queue_sample CACHE STRING "Example from examples/ to build") +set(TEST "" CACHE STRING "Test from test/osal/ to build instead of an example") +set(CMOCKA_DIR "$ENV{CMOCKA_DIR}" CACHE PATH "cmocka source tree (tests only)") + +if(TEST) + # cmocka needs signal.h, setenv() etc., which the minimal libc lacks: use the host libc. + set(EXTRA_CONF_FILE ${CMAKE_CURRENT_LIST_DIR}/test.conf) +endif() find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(dmosal_zephyr_app) -target_sources(app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../../examples/${EXAMPLE}.c) +set(REPO_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..) + +if(TEST) + target_sources(app PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/test_main.c + ${REPO_ROOT}/test/osal/${TEST}.c + ${CMOCKA_DIR}/src/cmocka.c) + # The test keeps its own main(); test_main.c calls it and reports the result. + set_source_files_properties(${REPO_ROOT}/test/osal/${TEST}.c + PROPERTIES COMPILE_DEFINITIONS main=dmosal_test_main) + # -std=c11 hides POSIX declarations (signal, setenv, strdup) that cmocka and the tests use. + # HAVE_CONFIG_H makes cmocka.c read cmocka/config.h. + target_compile_definitions(app PRIVATE _GNU_SOURCE HAVE_CONFIG_H) + target_include_directories(app PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/cmocka ${CMOCKA_DIR}/include ${REPO_ROOT}/test) +else() + target_sources(app PRIVATE ${REPO_ROOT}/examples/${EXAMPLE}.c) +endif() diff --git a/zephyr/app/cmocka/config.h b/zephyr/app/cmocka/config.h new file mode 100644 index 0000000..88b868a --- /dev/null +++ b/zephyr/app/cmocka/config.h @@ -0,0 +1,39 @@ +/* Minimal cmocka build configuration for Zephyr native_sim (host libc). */ +#ifndef DMOSAL_CMOCKA_CONFIG_H +#define DMOSAL_CMOCKA_CONFIG_H + +#define HAVE_ASSERT_H 1 +#define HAVE_INTTYPES_H 1 +#define HAVE_MALLOC_H 1 +#define HAVE_SETJMP_H 1 +#define HAVE_SIGNAL_H 1 +#define HAVE_STDARG_H 1 +#define HAVE_STDDEF_H 1 +#define HAVE_STDINT_H 1 +#define HAVE_STDIO_H 1 +#define HAVE_STDLIB_H 1 +#define HAVE_STRING_H 1 +#define HAVE_TIME_H 1 +#define HAVE_STRUCT_TIMESPEC 1 +#define HAVE_UINTPTR_T 1 +#define HAVE_CALLOC 1 +#define HAVE_EXIT 1 +#define HAVE_FPRINTF 1 +#define HAVE_SNPRINTF 1 +#define HAVE_VSNPRINTF 1 +#define HAVE_FREE 1 +#define HAVE_LONGJMP 1 +#define HAVE_MALLOC 1 +#define HAVE_MEMCPY 1 +#define HAVE_MEMSET 1 +#define HAVE_PRINTF 1 +#define HAVE_SETJMP 1 +#define HAVE_SIGNAL 1 +#define HAVE_STRCMP 1 +#define HAVE_STRCPY 1 +#define HAVE_GCC_THREAD_LOCAL_STORAGE 1 +#define HAVE_CLOCK_GETTIME 1 +#define HAVE_CLOCK_REALTIME 1 +#define WORDS_SIZEOF_VOID_P 8 + +#endif diff --git a/zephyr/app/test.conf b/zephyr/app/test.conf new file mode 100644 index 0000000..d1bcafa --- /dev/null +++ b/zephyr/app/test.conf @@ -0,0 +1,2 @@ +CONFIG_EXTERNAL_LIBC=y +CONFIG_MAIN_STACK_SIZE=16384 diff --git a/zephyr/app/test_main.c b/zephyr/app/test_main.c new file mode 100644 index 0000000..97e6384 --- /dev/null +++ b/zephyr/app/test_main.c @@ -0,0 +1,10 @@ +/* Runs the cmocka test whose main() was renamed to dmosal_test_main, then ends the + * native_sim process with the test result so CI can use the exit code. */ +#include + +extern int dmosal_test_main(void); + +int main(void) +{ + exit(dmosal_test_main() == 0 ? 0 : 1); +} diff --git a/zephyr/docker.sh b/zephyr/docker.sh index c25a1ca..c689e9c 100755 --- a/zephyr/docker.sh +++ b/zephyr/docker.sh @@ -5,9 +5,11 @@ # # zephyr/docker.sh build # build every examples/*.c into build-zephyr/ # zephyr/docker.sh run # run them all +# zephyr/docker.sh test # build + run every test/osal/*_test.c (cmocka) on native_sim # zephyr/docker.sh shell # interactive shell in the container # # EXAMPLE= limits build/run to one file from examples/. +# TEST= limits test to one file from test/osal/ (for example TEST=mutex_test). set -euo pipefail IMAGE=dmosal-zephyr:local @@ -21,7 +23,14 @@ else for f in "$REPO"/examples/*.c; do EXAMPLES+=("$(basename "$f" .c)"); done fi -DOCKER_RUN=(docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$REPO":/dmosal) +if [ -n "${TEST:-}" ]; then + TESTS=("$TEST") +else + TESTS=() + for f in "$REPO"/test/osal/*_test.c; do TESTS+=("$(basename "$f" .c)"); done +fi + +DOCKER_RUN=(docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -e CMOCKA_DIR=/opt/cmocka -v "$REPO":/dmosal) if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then docker build -t "$IMAGE" -f "$REPO/zephyr/Dockerfile" "$REPO/zephyr" @@ -40,6 +49,19 @@ run) "${DOCKER_RUN[@]}" "$IMAGE" "build-zephyr/$ex/zephyr/zephyr.exe" -stop_at=3 done ;; +test) + failed=() + for t in "${TESTS[@]}"; do + "${DOCKER_RUN[@]}" "$IMAGE" west build -b "$BOARD" -d "build-zephyr-test/$t" zephyr/app -p auto \ + -- -DTEST="$t" >/dev/null + echo "=== $t ===" + "${DOCKER_RUN[@]}" "$IMAGE" "build-zephyr-test/$t/zephyr/zephyr.exe" -stop_at=120 || failed+=("$t") + done + if [ ${#failed[@]} -ne 0 ]; then + echo "FAILED: ${failed[*]}" + exit 1 + fi + ;; shell) "${DOCKER_RUN[@]}" -it "$IMAGE" bash ;; From ac2076810dc467ef000fd6725d5402199856d633 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Sat, 24 Oct 2026 07:10:37 +0900 Subject: [PATCH 4/5] fixed CI error --- CMakeLists.txt | 12 ++++++------ README.md | 4 ++-- docs/README.md | 2 +- docs/architecture.md | 6 +++--- docs/lifetime.md | 2 +- docs/porting.md | 18 +++++++++--------- justfile | 2 +- source/zephyr/osal_queue.c | 2 +- zephyr/README.md | 4 ++-- 9 files changed, 26 insertions(+), 26 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 65e4880..542db1b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,12 +78,12 @@ set(osal_SRC source/osal_log.c source/osal_rm.c source/osal_tmcheck.c - source/posix/osal_mutex.c - source/posix/osal_queue.c - source/posix/osal_sem.c - source/posix/osal_task.c - source/posix/osal_time.c - source/posix/osal_timer.c + source/linux/osal_mutex.c + source/linux/osal_queue.c + source/linux/osal_sem.c + source/linux/osal_task.c + source/linux/osal_time.c + source/linux/osal_timer.c ) # Generated headers live under the build tree — never in the source tree. diff --git a/README.md b/README.md index c63bfcd..d03ad04 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ semantics across POSIX and RTOS platforms. Everything (tasks, mutexes, semaphores, queues, timers, ...) is allocated statically up front through a central resource manager, so there's no malloc on the hot path and the worst-case memory usage is known ahead of time. A -POSIX/Linux backend and a Zephyr backend are included; other targets can be +Linux backend and a Zephyr backend are included; other targets can be added behind the same API without breaking existing callers. ``` @@ -87,7 +87,7 @@ backend maps to Zephyr primitives. ## Examples Sample programs live in [examples/](examples/). Build them with `just build` -(CMake target `examples`) for POSIX, or with `zephyr/docker.sh build` for +(CMake target `examples`) for Linux, or with `zephyr/docker.sh build` for Zephyr. ## Documentation diff --git a/docs/README.md b/docs/README.md index 0293d82..2479012 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ Doxygen-generated API reference (built via `make doc`). | [architecture.md](architecture.md) | Layered design, subsystem overview, resource-manager pattern, callback threading model, backend model. | | [api-spec.md](api-spec.md) | Behavioral specification per subsystem — functions, states, error codes, invariants. | | [lifetime.md](lifetime.md) | Init/deinit ordering, resource ownership, when it is safe to use / release a resource. | -| [porting.md](porting.md) | How to add a new backend (e.g. an RTOS or bare-metal target). Reference: the POSIX backend. | +| [porting.md](porting.md) | How to add a new backend (e.g. an RTOS or bare-metal target). Reference: the Linux backend. | | [thread-safety.md](thread-safety.md) | Per-API table classifying every public call as thread-safe, single-owner, or init-thread-only. | If you're just getting started, read them in this order: diff --git a/docs/architecture.md b/docs/architecture.md index dbc2852..0edd6cf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,7 +18,7 @@ behaviour matters more than dynamic flexibility. | dmosal (public API) | +---------------+---------------+ | portable | backend | - | layer | (POSIX...) | + | layer | (Linux...) | +---------------+---------------+ | OS user-space API | +-------------------------------+ @@ -34,7 +34,7 @@ behaviour matters more than dynamic flexibility. across every backend (resource manager, LIFO free-list, error strings, logging, time-checkpoint utility, top-level init). - **Backend** — `source//*.c`. Only these files touch OS-specific - APIs. The POSIX backend under `source/posix/` is the reference + APIs. The Linux backend under `source/linux/` is the reference implementation. ## Subsystems @@ -93,7 +93,7 @@ Two subsystems invoke user code from a thread other than the caller's: - **Task** — `osal_task_create(cfg)` spawns a new thread that runs `cfg->task_handler`. The handler runs to completion on that thread. - **Timer** — the timer callback fires on a backend-managed context, not - the caller's thread. On the POSIX backend that means a fresh helper + the caller's thread. On the Linux backend that means a fresh helper thread per expiration (via `SIGEV_THREAD`). User code must not assume any particular thread identity inside the callback. diff --git a/docs/lifetime.md b/docs/lifetime.md index 0b5937f..18d87bf 100644 --- a/docs/lifetime.md +++ b/docs/lifetime.md @@ -133,7 +133,7 @@ create -> [started/stopped, callback fires 0+ times] -> stop -> delete create --opens or attaches--> [send/recv N times] --unlinks if creator--> delete ``` -- On the POSIX backend, `osal_queue_create` may either create the +- On the Linux backend, `osal_queue_create` may either create the underlying kernel queue or attach to an existing one with the same name. `osal_queue_delete` unlinks the kernel queue only when this handle originally created it. diff --git a/docs/porting.md b/docs/porting.md index e3608a4..5d3c1e3 100644 --- a/docs/porting.md +++ b/docs/porting.md @@ -5,7 +5,7 @@ under `source/`. The rest of the library — the resource manager, LIFO, logging, error strings, tmcheck, and top-level init — is portable C and unchanged across backends. -The reference implementation lives at `source/posix/` and targets Linux / +The reference implementation lives at `source/linux/` and targets Linux / glibc. This document walks through what a new backend must provide. ## Files a backend must implement @@ -31,7 +31,7 @@ For each function, honour the behavior documented in [lifetime.md](lifetime.md). Concretely: - **Pool ops must be thread-safe** — `_create` and `_delete` need to - serialize their pool touches. On POSIX we do this by threading the + serialize their pool touches. On Linux we do this by threading the shared `osal_mutex_t *mutex` through each `_man_t`'s `osal_rm_t`. Reuse that pattern via `OSAL_RM_USEROBJMAN_DECLARE` / `_INIT` if the target OS has a mutex primitive. @@ -43,7 +43,7 @@ For each function, honour the behavior documented in without a helper-thread mechanism, provide one (e.g. a dedicated timer dispatch task). - **`osal_timer_delete` must reject in-callback invocation.** Use the - `in_callback` bool pattern from the POSIX backend: set true around the + `in_callback` bool pattern from the Linux backend: set true around the callback invocation, `OSAL_RUNTIME_ASSERT` in delete if it's true. ## Steps @@ -61,13 +61,13 @@ For each function, honour the behavior documented in 2. **Add sources to `CMakeLists.txt`.** Extend the `osal_SRC` list to include `source/mybackend/*.c` when the target backend is selected. - The simplest scheme: add an option `DMOSAL_BACKEND=posix|mybackend` + The simplest scheme: add an option `DMOSAL_BACKEND=linux|mybackend` and switch the source glob accordingly. Only one backend is compiled in at a time. 3. **Implement `osal_mutex.c` first.** Everything else depends on it — the shared mutex from this pool is passed to every other subsystem's - init. See `source/posix/osal_mutex.c` for the reference: + init. See `source/linux/osal_mutex.c` for the reference: - `s_mutex_man` static holding the pool. - A bare backend-level mutex (`s_mutex_man.resrc_mutex`) is used inside `osal_mutex_create` because it can't recursively use its own @@ -77,11 +77,11 @@ For each function, honour the behavior documented in 4. **Implement `osal_sem.c`, `osal_task.c`, `osal_timer.c`, `osal_queue.c`, `osal_time.c`.** Each mirrors the same `_man_t` + `osal_rm_t` structure. - Copy the POSIX file and swap the OS calls. + Copy the Linux file and swap the OS calls. 5. **Wire in the build.** `CMakeLists.txt` is the only place where the backend directory is referenced. Neither the public headers nor the - portable `source/*.c` files hard-code POSIX. + portable `source/*.c` files hard-code Linux. 6. **Run the existing test suite.** All six cmocka tests exercise only the public API and should pass unchanged. If they fail, the failure @@ -96,8 +96,8 @@ Some behaviors are inherently backend-specific and may need adjustment: the microsecond field will just always show 0 — no code change needed. - **`osal_task_cfg_t` hints** — `stack_addr`, `stack_size`, `priority`, `name` are best-effort. Honour them if your target OS supports them. - The POSIX reference implementation ignores them. -- **Timer callback thread** — the POSIX backend spawns one helper thread + The Linux reference implementation ignores them. +- **Timer callback thread** — the Linux backend spawns one helper thread per expiration (`SIGEV_THREAD`). On memory-constrained targets, use a single dedicated timer-dispatch task that walks a pending list. diff --git a/justfile b/justfile index 0054a50..fadab1f 100644 --- a/justfile +++ b/justfile @@ -77,7 +77,7 @@ doc: configure format: pre-commit run clang-format --all-files -# Run clang-tidy over the portable + POSIX sources via pre-commit +# Run clang-tidy over the portable + Linux sources via pre-commit tidy: cmake -S . -B {{build_dir}} -DCMAKE_BUILD_TYPE={{build_type}} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON cmake --build {{build_dir}} --target dmosal -j{{jobs}} diff --git a/source/zephyr/osal_queue.c b/source/zephyr/osal_queue.c index ed73ddb..e4e4a4d 100644 --- a/source/zephyr/osal_queue.c +++ b/source/zephyr/osal_queue.c @@ -36,7 +36,7 @@ #include "osal_assert.h" #include "osal_config.h" -/* Shared by every handle created with the same name (mirrors POSIX mq_open attach). */ +/* Shared by every handle created with the same name (mirrors Linux mq_open attach). */ typedef struct { struct k_msgq msgq; char buf[CONFIG_DMOSAL_QUEUE_BUF_SIZE] __aligned(sizeof(void *)); diff --git a/zephyr/README.md b/zephyr/README.md index 25bd730..daadeee 100644 --- a/zephyr/README.md +++ b/zephyr/README.md @@ -12,8 +12,8 @@ normal Linux process, no real hardware). Only Docker is needed on the host. | `zephyr/app/` | Zephyr app (CMake + `prj.conf` only). Builds a file from `examples/`, shared with all platforms. | | `zephyr/docker.sh`, `zephyr/Dockerfile` | Slim build container + helper. | -The portable code (`source/*.c`) is shared with the POSIX build; do not edit it -for Zephyr. Use `source/posix/*.c` as the reference for behaviour. +The portable code (`source/*.c`) is shared with the Linux build; do not edit it +for Zephyr. Use `source/linux/*.c` as the reference for behaviour. ## Quick start From cd791957053d97bacc5b5464b2a1214ffcc4206e Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Sat, 24 Oct 2026 07:16:47 +0900 Subject: [PATCH 5/5] fixed tsan --- justfile | 3 ++- test/osal/tmcheck_test.c | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/justfile b/justfile index fadab1f..4ccef6a 100644 --- a/justfile +++ b/justfile @@ -54,7 +54,8 @@ check-asan: # Build and run tests with ThreadSanitizer check-tsan: just build_dir=build-tsan configure -DDMOSAL_ENABLE_TSAN=ON - just build_dir=build-tsan check + # TSan aborts with "unexpected memory mapping" when ASLR is on; disable it for the run. + setarch "$(uname -m)" -R just build_dir=build-tsan check # Build and run tests with gcov coverage, generate HTML report check-coverage: diff --git a/test/osal/tmcheck_test.c b/test/osal/tmcheck_test.c index 34667fa..c59f2b5 100644 --- a/test/osal/tmcheck_test.c +++ b/test/osal/tmcheck_test.c @@ -280,6 +280,7 @@ static void test_tmcheck_diff_ns_large(void **state) static int g_mt_idx[TMCHECK_MT_TASKS]; static bool g_mt_ok[TMCHECK_MT_TASKS]; +static osal_sem_t *g_mt_done; static void tmcheck_mt_task(void *arg) { @@ -299,6 +300,7 @@ static void tmcheck_mt_task(void *arg) osal_tmcheck_capture_ts(idx); ts = osal_tmcheck_get_captured_ts(idx); g_mt_ok[i] = (ts > 0); + osal_sem_post(g_mt_done); } static void test_tmcheck_multithread_capture(void **state) @@ -310,6 +312,8 @@ static void test_tmcheck_multithread_capture(void **state) memset(g_mt_idx, 0xff, sizeof(g_mt_idx)); memset(g_mt_ok, 0, sizeof(g_mt_ok)); + g_mt_done = osal_sem_create(); + assert_non_null(g_mt_done); for (i = 0; i < TMCHECK_MT_TASKS; i++) { memset(&cfg, 0, sizeof(cfg)); @@ -318,11 +322,11 @@ static void test_tmcheck_multithread_capture(void **state) tasks[i] = osal_task_create(&cfg); assert_non_null(tasks[i]); } + /* Backends differ on whether delete waits for the task, so wait for each to finish. */ + for (i = 0; i < TMCHECK_MT_TASKS; i++) { + assert_int_equal(osal_sem_waittime(g_mt_done, 5000000), OSAL_E_OK); + } for (i = 0; i < TMCHECK_MT_TASKS; i++) { - /* Backends differ on whether delete waits for the task, so wait explicitly. */ - for (int wait = 0; wait < 1000 && !g_mt_ok[i]; wait++) { - osal_usleep(1000); - } osal_task_delete(tasks[i]); } @@ -331,6 +335,7 @@ static void test_tmcheck_multithread_capture(void **state) assert_true(g_mt_idx[i] >= 0); osal_tmcheck_delete(g_mt_idx[i]); } + osal_sem_delete(g_mt_done); } static void test_tmcheck_print_diff(void **state)