59 Commits

Author SHA1 Message Date
anth64 5509c18b17 docs: update version in readme and add entry in changelog for 1.0.0-pre.12 2026-03-29 08:09:42 +02:00
anth64 cc39200917 build: change static library extension on Windows from .lib to .a for MinGW 2026-03-29 08:03:05 +02:00
anth64 95d7191fdd fix: use RTLD_GLOBAL when loading modules to expose host symbols 2026-03-19 07:38:49 +01:00
anth64 eb4b4c1f80 fix(build): default PREFIX to /usr in gmake.mk 2026-03-15 18:01:46 +01:00
anth64 7b7916911c chore: bump to pre.10 2026-03-14 17:31:06 +01:00
anth64 e46ec52c72 build(windows): use ar instead of lib for static library on MinGW 2026-03-14 13:22:31 +01:00
anth64 09528d60fd build(freebsd): fix path nesting 2026-03-13 08:03:14 +01:00
anth64 d1004d8840 chore(docs): update installation instructions and bump to pre.9 2026-03-12 22:44:44 +01:00
anth64 6fbd03f863 chore(build): add static library output and privilege-safe install
- debug and release targets now produce both shared and static libs
- obj dirs split into shared/ and static/ to keep fPIC objects isolated
- install no longer depends on release, guards with existence check instead
- build.sh handles privilege escalation for install/uninstall via doas or
  sudo, falling back gracefully if already root
- uninstall cleans up static lib alongside shared
2026-03-12 22:35:03 +01:00
anth64 0cd50a5781 fix: stale slot indices, cascade OOB, pending over-alloc, collect_dependents bounds, size_t format on Windows
- stk_poll: unify load loops to always append on compacted array, removing stale pre-compaction slot indices
- stk_poll: heap-allocate cascade_indices per iteration, removing fixed 256-slot stack bound
- stk_pending_retry: shrink module array to actual count after retry loop completes
- stk_collect_dependents: add capacity parameter and bounds guard before index write
- stk_log_modules: cast module_count to unsigned long for C89 portable %lu on Windows
2026-03-08 10:12:51 +01:00
anth64 0ebeafd4bb docs: update changelog and readme for stk_module_load fix 2026-03-07 17:07:37 +01:00
anth64 03cce766cd fix(module): add missing logging for deferred module dependencies 2026-03-07 17:04:41 +01:00
anth64 5b252d2b4e docs: update README and CHANGELOG for v1.0.0-pre.6 2026-03-07 14:52:26 +01:00
anth64 08c846d641 perf: batch stk_pending_add calls in stk_poll load loops
Replace per-module stk_pending_add() calls in the load holes loop and
append_modules loop with a single shared load_batch[load_count] buffer,
flushed via stk_pending_add_batch() once after both loops complete.
2026-03-07 14:51:10 +01:00
anth64 6ca46fe79f docs: update README and CHANGELOG for v1.0.0-pre.5 2026-03-07 14:41:20 +01:00
anth64 ba3a9dd163 perf: batch stk_init deferred module pending adds 2026-03-07 14:35:21 +01:00
anth64 a28c4818ab docs: update README and CHANGELOG for v1.0.0-pre.4 2026-03-07 14:25:00 +01:00
anth64 49ecd8fde5 perf: batch pending queue allocations in poll and retry
Replace per-module malloc calls in stk_pending_retry and stk_poll
with bulk allocations.

- stk_pending_retry: single realloc to module_count + stk_pending_count
  before the retry loop instead of realloc + 1 per module at attempt_load
- stk_poll unload loop: collect cascade deps into a batch array, call
  stk_pending_add_batch once after instead of stk_pending_add per module
- stk_poll cascade loop: same pattern, batch collect and add once per
  cascade iteration
2026-03-07 14:21:15 +01:00
anth64 06d04cf92b docs: update CHANGELOG and README for v1.0.0-pre.3 2026-03-07 12:16:47 +01:00
anth64 4250f91969 feat(stk.h, module, stk): improve dependency failure logging
- Add STK_MOD_DEP_LOG_BUFFER (2048) to stk.h for the dep failure message
buffer size.

- Add stk_log_dependency_failures(index, action) to module.c. Walks all
deps for the given module, skips satisfied ones, and builds a single
log line listing every unmet dep with its reason: "not found" or
"requires <constraint>, have <version>". The action parameter
("Deferring" / "Unloading") lets call sites produce contextually
appropriate messages:

  - Deferring 'test_mod_dep': unmet deps: test_mod (not found)
  - Unloading 'test_mod_dep': unmet deps: test_mod (not found), renderer (requires ^2.0.0, have 1.3.0)

- Replace the silent defer at init and the vague "unmet dependencies"
cascade log in stk_poll() with calls to stk_log_dependency_failures().
2026-03-07 11:34:23 +01:00
anth64 42a96f2bc0 feat(module, stk): dependency ordering, cascade unload, and pending queue
- Refactor the Kahn topological sort into a generic stk_kahn_sort() that
accepts a has_dep callback and an on_cycle callback, eliminating direct
coupling to stk_modules. stk_topo_sort() becomes a thin wrapper using
stk_loaded_has_dep() and stk_log_cycle(). stk_sort_load_order() uses the
same core via stk_batch_has_dep(), which opens tmp files to inspect deps
so simultaneous load events are processed dependency-first without needing
a retry cycle.

- Split stk_module_load() into stk_module_preload(), stk_module_activate(),
stk_module_discard(), and stk_validate_dependencies_single(). preload
handles library loading and metadata only; activate calls init; discard
cleans up without calling shutdown; validate_dependencies_single checks a
single module's deps. stk_module_load() composes these in order and does
not call init if deps are unmet.

- On UNLOAD events in stk_poll(), expand the unload set to include all
transitively dependent loaded modules via stk_collect_dependents(), sort
dependents-first via stk_sort_unload_order(), and unload in that order.
Modules unloaded due to expansion are queued to pending so they reload
automatically when their dependency returns. Remove trim_arrays, now
handled by the compact pass after the unload block.

- On LOAD events with unmet dependencies, add the tmp path to the pending
queue instead of dropping the module. stk_pending_retry() skips already
loaded entries and prunes entries whose file no longer exists. Move the
free_poll label above stk_pending_retry() so retry always runs regardless
of which path exits the event processing block.
2026-03-07 09:38:46 +01:00
anth64 55fcc019e4 chore(CHANGELOG.md): add v1.0.0-pre.2 entry 2026-03-06 22:44:04 +01:00
anth64 5d758c2998 fix(module.c): validate dependencies before calling init in stk_module_load 2026-03-06 22:41:31 +01:00
anth64 2e2c3d5e9e feat: phase 1 complete - dependency system, cascade unload, pending queue (v1.0.0-pre.1) 2026-03-06 22:13:35 +01:00
anth64 6477cde367 feat(module.c, stk.c): implement pending queue and cascade dependency resolution 2026-03-06 07:52:34 +01:00
anth64 4f4ae80a14 test: add test_mod_dep build targets and update test_mod with metadata 2026-03-05 00:10:06 +01:00
anth64 8e9007fdfe refactor(module.c, stk.c): reorder stk_mod_t fields largest to smallest 2026-03-04 23:37:15 +01:00
anth64 6e8df4e0e1 feat(stk.c): add module metadata logging on init, load, reload, and unload 2026-03-04 07:46:29 +01:00
anth64 f4c76f7b5a feat(module.c): log all dependency validation failures before returning error 2026-03-03 23:10:46 +01:00
anth64 5e7b41ab68 feat(module.c): log offending modules on circular dependency detection 2026-03-03 23:03:46 +01:00
anth64 9a4b5ee9ef refactor(stk.h, module.c, stk.c): switch deps to exported array and clean up types
- Move stk_dep_t to public header
- Rename stk_set_module_dependencies_fn to stk_set_module_deps_sym
- Change default deps symbol name from stk_mod_dependencies to stk_mod_deps
- Read deps as direct exported stk_dep_t array instead of function call
- Update sentinel check to .id[0] != '\0'
- Default module version to 0.0.0 if not provided or invalid
- Remove deps_func from union in stk_module_load
2026-03-03 22:53:37 +01:00
anth64 ceb389de0d feat(module.c, stk.c): expose dependency functions and integrate into init and poll
- Make stk_validate_dependencies and stk_topo_sort non static
- Add extern declarations in stk.c
- Run stk_validate_dependencies after module loads in stk_init
- Validation failure is fatal; unload all modules and return error code
- Run stk_topo_sort after validation, sort failure is logged but not fatal
- Add validate_deps label in stk_poll so all load/unload paths run validation
- Add dep error codes to stk_error_string
2026-03-03 22:29:59 +01:00
anth64 6faf33d944 feat(module.c): add topological sort with cycle detection 2026-03-03 22:21:04 +01:00
anth64 46f0f31ed7 feat(module.c): add dependency validation pass 2026-03-03 22:09:24 +01:00
anth64 e48d5f0d5b feat(module.c): add internal version parsing and constraint validation 2026-03-03 21:59:45 +01:00
anth64 dc014292cb refactor(module.c, stk.c): replace parallel arrays with stk_mod_t struct
- Consolidate handles, function pointers, metadata, and deps into single stk_mod_t
- Remove all separate metadata arrays and index mappings
- Simplify init, free, realloc, load, and unload significantly
- stk.c externs reduced to stk_modules and module_count
2026-03-03 21:42:43 +01:00
anth64 cb7c43f8af feat(module.c): add stk_dep_t struct and dependency arrays 2026-03-03 21:23:57 +01:00
anth64 6bddc6a888 refactor(module.c): extract common setter logic into stk_set_fn_name helper 2026-03-03 21:06:37 +01:00
anth64 7d23ddbc11 feat(module.c): add stk_mod_dependencies setter and default symbol name 2026-03-03 20:50:52 +01:00
anth64 9aba1a9ad8 docs(readme): add missing module metadata function documentation 2026-03-02 22:02:39 +01:00
anth64 d344e9bbd3 feat(stk.h): add dependency management defines and callbacks
Add error codes for dependency failures, constraint operators,
and setter for custom dependencies symbol name.
2026-03-02 21:53:58 +01:00
anth64 33de5e981e docs(README): update project status to 0.1.3 2026-02-26 07:37:54 +01:00
anth64 aefd962baf chore: bump version to 0.1.3 2026-02-25 22:45:58 +01:00
anth64 983f9b7393 feat(module.c): add optional module metadata support
- Add tight-packed arrays with index mappings for name, version, and description
- Resolve optional metadata symbols at load time via platform_get_symbol
- Add setter functions for metadata symbol name overrides
2026-02-25 22:31:22 +01:00
anth64 13067902a8 feat: add buffer constants and setters for optional module metadata 2026-02-25 21:37:05 +01:00
anth64 345cd35f19 chore: bump version to 0.1.2 2026-02-15 23:05:18 +01:00
anth64 c2b9571c2d fix(platform,core,test): fix Windows compatibility issues
- platform_mkdir now checks if directory exists before creating
- Add FILE_ATTRIBUTE_HIDDEN for dot-prefixed directories on Windows
- Create parent mods directory before temp directory
- Move module unload before copy in reload sequence to fix issue on Windows
- Force CMD shell in test makefile on Windows

Test makefile now uses cmd.exe instead of bash on Windows, fixing
syntax errors when running via build.bat.
2026-02-15 22:53:12 +01:00
anth64 3f7f216c92 chore: bump version to 0.1.1 and update documentation 2026-02-14 17:25:20 +01:00
anth64 70d50dda92 fix(logging): correct log level severity order
Reverse enum so lower values = less severe, higher = more severe.
This makes the filter check (level < min_log_level) work correctly.

- DEBUG (0) - least severe, filtered by default
- INFO (1) - default minimum level
- WARN (2) - warnings and above
- ERROR (3) - most severe, always shown

Fixes incorrect filtering where ERROR/WARN were being blocked.
2026-02-14 17:22:39 +01:00
anth64 70e9ec2fc3 chore: bump version to 0.1.0 and update documentation
- Update stk_version.h: 0.0.4 → 0.1.0
- Add CHANGELOG entry for 0.1.0 release
  - C89 compliance fixes
  - Flags bitfield system
  - Enhanced logging system
- Update README project status and API reference
- Document new logging functions and configuration
2026-02-14 17:06:50 +01:00
anth64 110b7ffca8 feat(logging)!: add enhanced logging system
BREAKING CHANGE: stk_log() signature changed to require log level

- Add log levels (ERROR, WARN, INFO, DEBUG) with runtime filtering
- Add timestamps (yyyy-mm-dd HH:MM:SS.mmm format)
- Add stk_set_log_output(), stk_set_log_prefix(), stk_set_log_level()
- Platform-specific timestamp: GetLocalTime (Windows), gettimeofday (POSIX)
- Consolidate duplicate platform includes into POSIX common block
- Setting log output to NULL disables logging.

Default: INFO level, "stk" prefix, stdout output. All internal messages
use appropriate severity levels. Single byte flag controls enable/disable.
2026-02-14 16:59:56 +01:00
anth64 26fb19a7f5 feat(core): add flags bitfield system
- Replace stk_initialized with stk_flags bitfield
- Add STK_FLAG_INITIALIZED (0x01) and STK_FLAG_LOGGING_ENABLED (0x02)
- Add stk_set_logging_enabled() and stk_is_logging_enabled() API
- Single byte for all boolean state and settings

Logging enabled by default, packs all flags into one byte for efficiency.
2026-02-14 12:39:13 +01:00
anth64 bcb1795218 fix(core)!: enforce strict C89 compliance
BREAKING CHANGE: Public API now uses unsigned char instead of uint8_t

- Remove stdint.h dependency (C99 feature, not C89, I am a fucking idiot)
- Replace uint8_t with unsigned char throughout codebase
- Affects stk_init() return type and internal functions
- Corrects unintended C99 dependency, restoring intended C89 compliance
2026-02-14 11:41:41 +01:00
anth64 fb0d8adb8f chore: bump version to 0.0.4 2026-02-11 00:22:59 +01:00
anth64 2c4d27f915 fix(linux): prevent segfault from invalid module indices during rapid reloads
When spamming file changes rapidly, inotify can report stale UNLOAD/RELOAD
events for modules that were already unloaded by previous events in the same
poll cycle. This caused is_mod_loaded() to return -1, which was then cast to
size_t (18446744073709551615) and used as an array index, causing segfaults.

Additionally, event counts were calculated before validation, causing loops to
run more iterations than valid indices were populated, reading garbage values.

Changes:
- stk.c: Check if is_mod_loaded() returns valid index (>= 0) before adding
  to unload/reload lists
- stk.c: Reset and recalculate counts after populating arrays with only valid
  indices to prevent loop overrun
- Skip processing events for modules that are no longer loaded

This completes the Linux stability fixes started in v0.0.2.
2026-02-11 00:17:33 +01:00
anth64 96fb957991 chore: bump version to 0.0.3 2026-02-09 23:03:35 +01:00
anth64 0cbee45ad2 fix(platform): replace strncpy with memmove to silence -Wrestrict warning 2026-02-09 22:53:42 +01:00
anth64 142a61a843 chore: bump version to 0.0.2 2026-02-09 22:32:55 +01:00
anth64 f83f2051ef fix(linux): prevent segfaults during rapid module reloads
Fixes multiple issues causing segfaults when hot-reloading modules
on Linux, particularly when file changes are detected rapidly:

- Enable is_file_ready() check on Linux to prevent loading partially-written
  shared libraries (previously only used on Windows/BSD)
- Fix event deduplication on Linux to actually remove duplicate inotify events
  instead of just marking them, preventing double-free on same module
- Reorder reload operations to unload old module only after successfully
  copying new version, avoiding invalid state when copy fails

Changes:
- platform.c: Remove __linux__ guards around is_file_ready() function
- platform.c: Add compaction step after deduplication to remove -1 entries
- stk.c: Move module unload to after platform_copy_file() in reload loop

These changes make Linux hot-reload as robust as Windows/BSD implementations.
2026-02-09 22:25:02 +01:00
18 changed files with 2158 additions and 432 deletions
+214 -1
View File
@@ -7,6 +7,199 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.0-pre.12] - 2026-03-29
### Fixed
- `gmake.mk`: changed static library extension on Windows from `.lib` to `.a` for MinGW compatibility
- `module_loader`: use `RTLD_GLOBAL` when loading modules to correctly expose host symbols to dynamic libraries
## [1.0.0-pre.11] - 2026-03-15
### Fixed
- `gmake.mk`: `PREFIX` now defaults to `/usr` on Linux instead of `/usr/local`
## [1.0.0-pre.10] - 2026-03-14
### Fixed
- `bmake.mk`: all target and object paths now anchored with `${.CURDIR}` to fix incorrect path nesting when building from outside the project root; substitution delimiters switched to `|` to avoid conflicts with path separators; compile rules now emit `-MT`/`-MF` for correct dependency file targeting; removed stale `.-include` directives for `.d` files; release lib rules now use `${.TARGET}` consistently
- `gmake.mk`: removed `AR`/`ARFLAGS_STATIC` variables and the `ifeq`-guarded static lib rules; both debug and release static lib rules now use `ar rcs` directly, as MinGW ships `ar` and `lib.exe` was incorrect
## [1.0.0-pre.9] - 2026-03-12
### Changed
- `debug` and `release` targets now produce both a shared and static library alongside each other
- obj directories split into `shared/` and `static/` subdirs to keep `-fPIC` objects isolated
- `install` no longer depends on `release`; guards with an existence check and exits with a clear message if artifacts are missing
- `uninstall` removes static lib alongside shared lib
- `build.sh`: `install` and `uninstall` now handle privilege escalation automatically via `doas` or `sudo`; skips escalation if already root
## [1.0.0-pre.8] - 2026-03-08
### Fixed
- `stk_poll()`: stale index corruption when simultaneous load and unload events occurred in the same poll cycle. New modules were previously loaded into pre-compaction slot indices; they now always append to the compacted array via `module_count + successful_appends`, matching the `append_modules` path. The two load loops have been unified into one.
- `stk_poll()`: `cascade_indices` was a fixed-size stack array of `STK_PATH_MAX` (256) elements with no bounds check. It is now heap-allocated to `module_count` entries per iteration, eliminating the silent overflow risk.
- `stk_pending_retry()`: module array was pre-allocated to `module_count + stk_pending_count` but never shrunk when fewer entries loaded than were pending. Now calls `stk_module_realloc_memory(module_count)` after the retry loop completes.
- `stk_collect_dependents()`: missing bounds guard on the `indices` write: `(*count)++` had no check against the buffer capacity before writing. Added `capacity` parameter (passed as `module_count` from the call site) and a guard that skips the write if the capacity is reached, preventing a silent overflow.
- `stk_log_modules()`: `%lu` format specifier used with `size_t` is undefined behaviour on platforms where `unsigned long` is narrower than `size_t` (e.g. MSVC 64-bit). Fixed with an explicit `(unsigned long)` cast, preserving C89 compatibility.
## [1.0.0-pre.7] - 2026-03-07
### Fixed
- `stk_module_load()`: added missing dependency failure logging when a module is deferred to the pending queue.
## [1.0.0-pre.6] - 2026-03-07
### Changed
- `stk_poll()`: replaced per-module `stk_pending_add()` calls inside the load holes and `append_modules` loops with batch collection followed by a single `stk_pending_add_batch()` call after both loops complete.
## [1.0.0-pre.5] - 2026-03-07
### Changed
- `stk_init()`: replaced per-module `stk_pending_add()` calls inside the deferred module loop with batch collection followed by a single `stk_pending_add_batch()` call after the loop.
## [1.0.0-pre.4] - 2026-03-07
### Changed
- `stk_pending_retry()`: replaced per-module `realloc` at `attempt_load` with a single bulk allocation upfront (`module_count + stk_pending_count`) before the retry loop.
- `stk_poll()`: replaced per-module `stk_pending_add()` calls inside the unload and cascade loops with batch collection followed by a single `stk_pending_add_batch()` call after each loop.
## [1.0.0-pre.3] - 2026-03-07
### Added
- `STK_MOD_DEP_LOG_BUFFER` (2048) added to `stk.h`
### Changed
- Dependency failure logging now emits a single line per module listing all unmet deps with their reason: `not found` or `requires <constraint>, have <version>`
- `Deferring 'test_mod_dep': unmet deps: test_mod (not found)`
- `Unloading 'test_mod_dep': unmet deps: test_mod (not found), renderer (requires ^2.0.0, have 1.3.0)`
- Silent defer at `stk_init` and vague `"unmet dependencies"` cascade log in `stk_poll` replaced with `stk_log_dependency_failures()`
- Kahn topological sort refactored into generic `stk_kahn_sort()` accepting a `has_dep` callback and an `on_cycle` callback, decoupling it from `stk_modules`. `stk_topo_sort()` is now a thin wrapper. `stk_sort_load_order()` uses the same core via `stk_batch_has_dep()`, inspecting tmp files so simultaneous load events are processed dependency-first without a retry cycle
- `stk_module_load()` split into `stk_module_preload()`, `stk_module_activate()`, `stk_module_discard()`, and `stk_validate_dependencies_single()`. Init is not called if deps are unmet
- On UNLOAD events in `stk_poll()`, the unload set is expanded to include all transitively dependent modules via `stk_collect_dependents()`, sorted dependents-first via `stk_sort_unload_order()`. Modules unloaded due to expansion are queued to pending
- On LOAD events with unmet dependencies, the tmp path is added to the pending queue instead of being dropped
- `stk_pending_retry()` now skips already-loaded entries and prunes entries whose file no longer exists
- `free_poll` label moved above `stk_pending_retry()` so retry always runs regardless of exit path
## [1.0.0-pre.2] - 2026-03-06
### Fixed
- Module `init` is no longer called if dependencies are not satisfied. The library handle is unloaded and a dep error is returned.
## [1.0.0-pre.1] - 2026-03-06
### Added
- **Dependency System**: Full module dependency declaration and resolution
- Modules declare dependencies via an exported `stk_mod_deps` symbol, no stk headers required in modules, only the memory layout contract must be respected (`{ char[64], char[32] }` sentinel-terminated array)
- Version constraint operators: `=` (exact), `>=` (minimum, default), `^` (compatible — same major, greater or equal minor/patch)
- Version defaults to `0.0.0` if not provided or unparseable
- Dependency validation on `stk_init` and every `stk_poll` cycle
- Topological sort with cycle detection, load and unload ordering enforced
- All dependency failures logged before returning
- **Cascade Unload**: When a module is removed, all dependents cascade unload automatically and are added to the pending queue. Cascades repeat until the loaded set is stable
- **Pending Queue**: Modules that fail dependency validation are deferred. When their dependencies become available they are retried and loaded automatically. Entries are removed if their file is deleted
### Changed
- `stk_set_module_dependencies_fn` renamed to `stk_set_module_deps_sym`. Deps are now an exported array symbol, not a function
- Default deps symbol name changed from `stk_mod_dependencies` to `stk_mod_deps`
- `stk_dep_t` added to public header `stk.h`
- `stk_mod_t` field order changed to largest-to-smallest for correct memory alignment with zero padding waste
- Dependency validation failure during `stk_init` no longer fatal, affected modules are deferred to the pending queue
- Poll cycle now logs the full loaded module list as a single summary after any event that changes the loaded set, instead of logging each module individually
### Notes
- This release marks Phase 1 complete: hot-reloading foundation with dependency management
- Memory layout contract for `stk_mod_deps`: sentinel-terminated array of `{ char[64], char[32] }`. Modules do not need to include `stk.h`, define the struct inline or via your engine's own header as long as the layout matches
## [0.1.3] - 2026-02-25
### Added
- **Module Metadata**: Optional metadata support for modules
- `stk_mod_name` - human-readable module name
- `stk_mod_version` - semantic version string
- `stk_mod_description` - module description
- All three are optional, missing symbols are not errors
- Metadata stored in tight-packed arrays with index mappings
- Setter functions to override default symbol names before `stk_init()`
## [0.1.2] - 2026-02-15
### Fixed
- **Windows**
- Module reload now works correctly, idk how it was broken
- Temp directory creation fixed
- `.tmp` directory now hidden via FILE_ATTRIBUTE_HIDDEN
- Test build fixed
- Force cmd.exe shell in gmake.mk
- Fix bash syntax errors when running build.bat
- **All Platforms**: platform_mkdir now checks if directory exists before creating
## [0.1.1] - 2026-02-14
### Fixed
- **Logging**: Corrected log level severity order in enum
- Reversed order so DEBUG (0) < INFO (1) < WARN (2) < ERROR (3)
- Fixes filtering logic where ERROR/WARN were incorrectly blocked
- Default INFO level now properly shows INFO, WARN, and ERROR while filtering DEBUG
## [0.1.0] - 2026-02-14
### Fixed
- **C89 Compliance**: Removed stdint.h dependency (C99 feature)
- Replaced all uint8_t with unsigned char throughout codebase
- Ensures strict C89 compliance for maximum portability
### Added
- **Flags system**: Centralized bitfield for boolean state and settings
- Replaces stk_initialized with stk_flags for efficient memory usage
- Single byte packs all boolean flags (STK_FLAG_INITIALIZED, STK_FLAG_LOGGING_ENABLED)
- Runtime-changeable logging control via stk_set_logging_enabled()
- **Enhanced logging system**: Complete rewrite with modern features
- Log levels: ERROR, WARN, INFO, DEBUG with runtime filtering
- Timestamps: yyyy-mm-dd HH:MM:SS.mmm format (platform-specific implementation)
- Configurable log output stream (stk_set_log_output)
- Configurable log prefix (stk_set_log_prefix, defaults to "stk")
- Configurable minimum log level (stk_set_log_level, defaults to INFO)
- Platform abstraction for timestamps (GetLocalTime on Windows, gettimeofday on POSIX)
### Changed
- **BREAKING**: stk_log() signature changed from stk_log(FILE *fp, ...) to stk_log(stk_log_level_t level, ...)
- **BREAKING**: All uint8_t types replaced with unsigned char
- All internal STK messages now use appropriate log levels
- Setting log output to NULL disabls logging
### Notes
- This release completes Phase 1 logging improvements
- Dependency management still in progress for Phase 1 completion
## [0.0.4] - 2026-02-11
### Fixed
- **Linux**: Fixed segfault from invalid module indices during extremely rapid file changes
- Added validation check to skip stale UNLOAD/RELOAD events for already-unloaded modules
- Prevents is_mod_loaded() returning -1 from being used as array index (SIZE_MAX)
- Fixed event count mismatch where loops would run more iterations than valid indices populated
- Completes the Linux hot-reload stability fixes from v0.0.2
## [0.0.3] - 2026-02-10
### Fixed
- **Compilation**: Fixed GCC `-Wrestrict` warning in Linux directory watching code by replacing `strncpy` with `memmove` for overlapping memory operations
- Ensures clean compilation without warnings while maintaining identical runtime behavior
- Uses semantically correct function for moving memory within the same buffer
## [0.0.2] - 2026-02-09
### Fixed
- **Linux**: Fixed segfaults during rapid module reloads when file changes are detected in quick succession
- Enabled file readiness checks on Linux (previously only used on Windows/BSD) to prevent loading partially-written shared libraries
- Fixed inotify event deduplication to actually remove duplicate events instead of just marking them
- Reordered reload operations to only unload old module after successfully copying new version, preventing invalid state when copy fails
- **All platforms**: Improved reload safety by deferring module unload until after successful file copy
### Changed
- Made `is_file_ready()` check available on all Unix platforms (was previously excluded on Linux)
## [0.0.1] - 2026-02-01
### Added
@@ -25,5 +218,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Dependency management and versioning not yet implemented
- API is unstable and subject to change in future releases
[Unreleased]: https://github.com/anth64/stk/compare/v0.0.1...HEAD
[Unreleased]: https://github.com/anth64/stk/compare/v1.0.0-pre.12...HEAD
[1.0.0-pre.12]: https://github.com/anth64/stk/compare/v1.0.0-pre.11...v1.0.0-pre.12
[1.0.0-pre.11]: https://github.com/anth64/stk/compare/v1.0.0-pre.10...v1.0.0-pre.11
[1.0.0-pre.10]: https://github.com/anth64/stk/compare/v1.0.0-pre.9...v1.0.0-pre.10
[1.0.0-pre.9]: https://github.com/anth64/stk/compare/v1.0.0-pre.8...v1.0.0-pre.9
[1.0.0-pre.8]: https://github.com/anth64/stk/compare/v1.0.0-pre.7...v1.0.0-pre.8
[1.0.0-pre.7]: https://github.com/anth64/stk/compare/v1.0.0-pre.6...v1.0.0-pre.7
[1.0.0-pre.6]: https://github.com/anth64/stk/compare/v1.0.0-pre.5...v1.0.0-pre.6
[1.0.0-pre.5]: https://github.com/anth64/stk/compare/v1.0.0-pre.4...v1.0.0-pre.5
[1.0.0-pre.4]: https://github.com/anth64/stk/compare/v1.0.0-pre.3...v1.0.0-pre.4
[1.0.0-pre.3]: https://github.com/anth64/stk/compare/v1.0.0-pre.2...v1.0.0-pre.3
[1.0.0-pre.2]: https://github.com/anth64/stk/compare/v1.0.0-pre.1...v1.0.0-pre.2
[1.0.0-pre.1]: https://github.com/anth64/stk/compare/v0.1.3...v1.0.0-pre.1
[0.1.3]: https://github.com/anth64/stk/releases/tag/v0.1.3
[0.1.2]: https://github.com/anth64/stk/releases/tag/v0.1.2
[0.1.1]: https://github.com/anth64/stk/releases/tag/v0.1.1
[0.1.1]: https://github.com/anth64/stk/releases/tag/v0.1.1
[0.1.0]: https://github.com/anth64/stk/releases/tag/v0.1.0
[0.0.4]: https://github.com/anth64/stk/releases/tag/v0.0.4
[0.0.3]: https://github.com/anth64/stk/releases/tag/v0.0.3
[0.0.2]: https://github.com/anth64/stk/releases/tag/v0.0.2
[0.0.1]: https://github.com/anth64/stk/releases/tag/v0.0.1
+79 -14
View File
@@ -14,7 +14,7 @@ It is designed to run on modern systems running POSIX and Windows using C89.
- **Hot-swapping** of modules at runtime
- **Cross-platform** (Linux, BSD, Windows, macOS)
- **Optional WASM support** for multi-language modules (planned)
- **Developer tools**: lightweight metadata, logging/tracing, and dependency management (in progress)
- **Developer tools**: lightweight metadata, logging/tracing, and dependency management
- **Minimal, portable API**
---
@@ -43,19 +43,19 @@ build.bat debug release
#### Unix (Linux/BSD/macOS)
```bash
sudo ./build.sh install
./build.sh install
```
Installs to `/usr/local` by default. Use `PREFIX` to customize:
Installs to `/usr` on Linux, `/usr/local` on BSD/macOS by default. Use `PREFIX` to customize:
```bash
sudo ./build.sh PREFIX=$HOME/.local install
./build.sh PREFIX=$HOME/.local install
```
#### Windows
```
build.bat release
```
* Once finished building, copy the headers from `include/` to `your_project/include/stk/` and `bin/release/stk.dll` to your project's lib directory.
* Once finished building, copy the headers from `include/` to `your_project/include/stk/` and copy `bin/release/stk.dll` or `bin/release/stk.lib` to your project's `lib` directory, depending on whether you are linking dynamically or statically.
---
@@ -131,6 +131,42 @@ cc -shared -o my_module.dll my_module.c
Place the compiled module in the `mods/` directory (default), and `stk` will automatically load it and watch for changes.
### Module Metadata
Modules can optionally export metadata functions:
```c
const char *stk_mod_name(void) { return "My Module"; }
const char *stk_mod_version(void) { return "1.2.0"; }
const char *stk_mod_description(void) { return "Does something useful"; }
```
All three are optional. Version defaults to `0.0.0` if not exported or unparseable.
### Declaring Dependencies
Modules declare dependencies via an exported sentinel-terminated array. No stk headers are required in the module. The only requirement is that the memory layout matches: `{ char[64], char[32] }`.
```c
typedef struct {
char id[64];
char version[32];
} dep_t;
dep_t stk_mod_deps[] = {
{ "physics", ">=2.0.0" },
{ "renderer", "^1.0.0" },
{ "", "" } /* sentinel */
};
```
Version constraint operators:
- `=1.0.0` exact match
- `>=2.0.0` minimum version, also the default if no operator is specified
- `^1.0.0` same major, greater or equal minor/patch
If a dependency is removed at runtime, all affected modules are unloaded and queued. When the dependency comes back, they load automatically.
### Configuration
```c
@@ -146,6 +182,18 @@ stk_set_module_init_fn("my_init");
/* Set custom shutdown function name (default: "stk_mod_shutdown") */
stk_set_module_shutdown_fn("my_shutdown");
/* Set function name to get module name (default: "stk_mod_name") */
stk_set_module_name_fn("my_mod_name");
/* Set function name to get module version (default: "stk_mod_version") */
stk_set_module_version_fn("my_mod_version");
/* Set function name to get module description (default: "stk_mod_description") */
stk_set_module_description_fn("my_mod_description");
/* Set deps array symbol name (default: "stk_mod_deps") */
stk_set_module_deps_sym("my_mod_deps");
/*
* All the above functions must be called before stk_init()
* if the defaults need to be changed.
@@ -156,7 +204,7 @@ stk_init();
### API Reference
#### Initialization
- `uint8_t stk_init(void)` - Initialize stk, returns `STK_INIT_SUCCESS` on success
- `unsigned char stk_init(void)` - Initialize stk, returns `STK_INIT_SUCCESS` on success
- `void stk_shutdown(void)` - Shutdown and cleanup all modules
#### Runtime
@@ -168,24 +216,41 @@ stk_init();
- `void stk_set_tmp_dir_name(const char *name)` - Set temp directory name
- `void stk_set_module_init_fn(const char *name)` - Set module init function name
- `void stk_set_module_shutdown_fn(const char *name)` - Set module shutdown function name
- `void stk_set_module_name_fn(const char *name);` - Set module name function name
- `void stk_set_module_version_fn(const char *name);` - Set module version function name
- `void stk_set_module_description_fn(const char *name);` - Set module description function name
- `void stk_set_module_deps_sym(const char *name)` - Set module deps array symbol name (default: `stk_mod_deps`)
#### Logging
- `void stk_set_logging_enabled(unsigned char enabled)` - Enable/disable all logging
- `unsigned char stk_is_logging_enabled(void)` - Query logging state
- `void stk_set_log_output(FILE *fp)` - Set log output stream (default: stdout, NULL disables)
- `void stk_set_log_prefix(const char *prefix)` - Set log prefix (default: "stk")
- `void stk_set_log_level(stk_log_level_t level)` - Set minimum log level (default: INFO)
**Log Levels:** `STK_LOG_ERROR`, `STK_LOG_WARN`, `STK_LOG_INFO`, `STK_LOG_DEBUG`
---
## Project Status
**Current Version:** 0.0.1 (Pre-release)
This is an early release proving the core hot-reload foundation. Phase 1 is still in progress.
**Current Version:** 1.0.0-pre.12
### What Works
- Cross-platform module loading and hot-reloading
- File watching (inotify/kqueue/FindFirstFile)
- Basic error handling
- Robust hot-reload even during extremely rapid file changes
- Enhanced logging with levels, timestamps, and filtering
- Runtime-configurable logging behavior
- Optional module metadata (name, version, description)
- Dependency declaration, validation, and versioning
- Detailed dependency failure logging (missing ids, version mismatches)
- Cascade unload when dependencies are removed
- Pending queue with automatic retry when deps become available
- Topological sort with cycle detection
### In Progress (Phase 1)
- Complete logging system (log levels, verbosity, output configuration)
- Module metadata (name, version, description)
- Dependency management and versioning
### Phase 2
- WASM module support
See [CHANGELOG.md](CHANGELOG.md) for detailed release notes.
+36 -18
View File
@@ -12,44 +12,59 @@ FULL_LIB = lib${LIB_NAME}.dylib
FULL_LIB = lib${LIB_NAME}.so
.endif
STATIC_LIB = lib${LIB_NAME}.a
LDFLAGS_PLAT = -ldl
CFLAGS_PLAT = -fPIC
CFLAGS_BASE = -Wall -Wpedantic -I${.CURDIR}/${INC_DIR} -std=c89 ${CFLAGS_PLAT}
CFLAGS_STATIC =
.PHONY: all debug release clean test install uninstall
all: debug
OBJS_DEBUG = ${SRCS:S/^src\//obj\/debug\//:S/.c$/.o/}
OBJS_RELEASE = ${SRCS:S/^src\//obj\/release\//:S/.c$/.o/}
OBJS_DEBUG_SHARED = ${SRCS:S|^src/|${.CURDIR}/obj/debug/shared/|:S/.c$/.o/}
OBJS_DEBUG_STATIC = ${SRCS:S|^src/|${.CURDIR}/obj/debug/static/|:S/.c$/.o/}
OBJS_RELEASE_SHARED = ${SRCS:S|^src/|${.CURDIR}/obj/release/shared/|:S/.c$/.o/}
OBJS_RELEASE_STATIC = ${SRCS:S|^src/|${.CURDIR}/obj/release/static/|:S/.c$/.o/}
debug: ${BIN_DIR}/debug/${FULL_LIB}
release: ${BIN_DIR}/release/${FULL_LIB}
debug: ${.CURDIR}/${BIN_DIR}/debug/${FULL_LIB} ${.CURDIR}/${BIN_DIR}/debug/${STATIC_LIB}
release: ${.CURDIR}/${BIN_DIR}/release/${FULL_LIB} ${.CURDIR}/${BIN_DIR}/release/${STATIC_LIB}
${BIN_DIR}/debug/${FULL_LIB}: ${OBJS_DEBUG}
${.CURDIR}/${BIN_DIR}/debug/${FULL_LIB}: ${OBJS_DEBUG_SHARED}
@mkdir -p ${.TARGET:H}
${CC} -shared -o ${.TARGET} ${.ALLSRC} ${LDFLAGS_PLAT}
${BIN_DIR}/release/${FULL_LIB}: ${OBJS_RELEASE}
@mkdir -p ${.CURDIR}/${BIN_DIR}/release
${CC} -shared -s -o ${.CURDIR}/${BIN_DIR}/release/${FULL_LIB} ${.ALLSRC} ${LDFLAGS_PLAT}
${.CURDIR}/${BIN_DIR}/debug/${STATIC_LIB}: ${OBJS_DEBUG_STATIC}
@mkdir -p ${.TARGET:H}
ar rcs ${.TARGET} ${.ALLSRC}
${.CURDIR}/${BIN_DIR}/release/${FULL_LIB}: ${OBJS_RELEASE_SHARED}
@mkdir -p ${.TARGET:H}
${CC} -shared -s -o ${.TARGET} ${.ALLSRC} ${LDFLAGS_PLAT}
${.CURDIR}/${BIN_DIR}/release/${STATIC_LIB}: ${OBJS_RELEASE_STATIC}
@mkdir -p ${.TARGET:H}
ar rcs ${.TARGET} ${.ALLSRC}
.for _src in ${SRCS}
_obj_base = ${_src:S/^src\///:S/.c$/.o/}
obj/debug/${_obj_base}: ${_src}
${.CURDIR}/obj/debug/shared/${_src:S|^src/||:S/.c$/.o/}: ${.CURDIR}/${_src}
@mkdir -p ${.TARGET:H}
${CC} ${CFLAGS_BASE} -g -O0 -MMD -MP -c ${.ALLSRC} -o ${.TARGET}
${CC} ${CFLAGS_BASE} -g -O0 -MMD -MP -MT ${.TARGET} -MF ${.TARGET:S/.o$/.d/} -c ${.ALLSRC} -o ${.TARGET}
obj/release/${_obj_base}: ${_src}
${.CURDIR}/obj/debug/static/${_src:S|^src/||:S/.c$/.o/}: ${.CURDIR}/${_src}
@mkdir -p ${.TARGET:H}
${CC} ${CFLAGS_BASE} -O2 -MMD -MP -c ${.ALLSRC} -o ${.TARGET}
${CC} ${CFLAGS_BASE} ${CFLAGS_STATIC} -g -O0 -MMD -MP -MT ${.TARGET} -MF ${.TARGET:S/.o$/.d/} -c ${.ALLSRC} -o ${.TARGET}
${.CURDIR}/obj/release/shared/${_src:S|^src/||:S/.c$/.o/}: ${.CURDIR}/${_src}
@mkdir -p ${.TARGET:H}
${CC} ${CFLAGS_BASE} -O2 -MMD -MP -MT ${.TARGET} -MF ${.TARGET:S/.o$/.d/} -c ${.ALLSRC} -o ${.TARGET}
${.CURDIR}/obj/release/static/${_src:S|^src/||:S/.c$/.o/}: ${.CURDIR}/${_src}
@mkdir -p ${.TARGET:H}
${CC} ${CFLAGS_BASE} ${CFLAGS_STATIC} -O2 -MMD -MP -MT ${.TARGET} -MF ${.TARGET:S/.o$/.d/} -c ${.ALLSRC} -o ${.TARGET}
.endfor
.-include "obj/debug/*.d"
.-include "obj/release/*.d"
clean:
rm -rf ${.CURDIR}/${OBJ_DIR} ${.CURDIR}/${BIN_DIR}
@@ -57,13 +72,16 @@ test: debug
@echo "=== Building and running stk tests ==="
cd ${.CURDIR}/test && ${MAKE} -f bmake.mk
install: release
install:
@test -f ${.CURDIR}/${BIN_DIR}/release/${FULL_LIB} || { echo "Run 'make -f bmake.mk release' before installing."; exit 1; }
install -d ${LIBDIR} ${INCDIR}/stk
install -m 755 ${.CURDIR}/${BIN_DIR}/release/${FULL_LIB} ${LIBDIR}/
install -m 644 ${.CURDIR}/${BIN_DIR}/release/${STATIC_LIB} ${LIBDIR}/
install -m 644 ${.CURDIR}/${INC_DIR}/stk.h ${INCDIR}/stk/
install -m 644 ${.CURDIR}/${INC_DIR}/stk_version.h ${INCDIR}/stk/
install -m 644 ${.CURDIR}/${INC_DIR}/stk_log.h ${INCDIR}/stk/
uninstall:
rm -f ${LIBDIR}/${FULL_LIB}
rm -f ${LIBDIR}/${STATIC_LIB}
rm -rf ${INCDIR}/stk
+29 -1
View File
@@ -8,4 +8,32 @@ else
MK_FILE="bmake.mk"
fi
make -f "$MK_FILE" "$@"
HAS_INSTALL=0
HAS_UNINSTALL=0
for arg in "$@"; do
case "$arg" in
install) HAS_INSTALL=1 ;;
uninstall) HAS_UNINSTALL=1 ;;
esac
done
if [ "$HAS_INSTALL" = "1" ] || [ "$HAS_UNINSTALL" = "1" ]; then
if [ "$HAS_INSTALL" = "1" ]; then
make -f "$MK_FILE" release
fi
if [ "$(id -u)" = "0" ]; then
PRIV=""
elif command -v doas > /dev/null 2>&1; then
PRIV="doas"
elif command -v sudo > /dev/null 2>&1; then
PRIV="sudo"
else
echo "error: install requires root. neither doas nor sudo found." >&2
exit 1
fi
$PRIV make -f "$MK_FILE" "$@"
else
make -f "$MK_FILE" "$@"
fi
+1
View File
@@ -4,6 +4,7 @@ OBJ_DIR = obj
BIN_DIR = bin
LIB_NAME = stk
STATIC_LIB = lib$(LIB_NAME).a
SRCS = src/module.c \
src/platform.c \
+36 -11
View File
@@ -3,14 +3,18 @@ include config.mk
ifeq ($(OS),Windows_NT)
SHELL := cmd.exe
FULL_LIB := $(LIB_NAME).dll
STATIC_LIB := lib$(LIB_NAME).a
LDFLAGS_PLAT :=
CFLAGS_PLAT :=
CFLAGS_STATIC :=
MKDIR = if not exist $(subst /,\,$(1)) mkdir $(subst /,\,$(1))
RMDIR = if exist $(subst /,\,$(1)) rd /s /q $(subst /,\,$(1))
else
FULL_LIB := lib$(LIB_NAME).so
STATIC_LIB := lib$(LIB_NAME).a
LDFLAGS_PLAT := -ldl
CFLAGS_PLAT := -fPIC
CFLAGS_STATIC :=
MKDIR = mkdir -p $(1)
RMDIR = rm -rf $(1)
endif
@@ -18,7 +22,7 @@ endif
RELEASE_LDFLAGS := -s
CFLAGS_BASE := -Wall -Wpedantic -I$(INC_DIR) -std=c89 $(CFLAGS_PLAT)
PREFIX ?= /usr/local
PREFIX ?= /usr
LIBDIR ?= $(PREFIX)/lib
INCDIR ?= $(PREFIX)/include
@@ -26,29 +30,47 @@ INCDIR ?= $(PREFIX)/include
all: debug
debug: $(BIN_DIR)/debug/$(FULL_LIB)
release: $(BIN_DIR)/release/$(FULL_LIB)
debug: $(BIN_DIR)/debug/$(FULL_LIB) $(BIN_DIR)/debug/$(STATIC_LIB)
release: $(BIN_DIR)/release/$(FULL_LIB) $(BIN_DIR)/release/$(STATIC_LIB)
# Debug Rules
$(BIN_DIR)/debug/$(FULL_LIB): $(SRCS:src/%.c=obj/debug/%.o)
$(BIN_DIR)/debug/$(FULL_LIB): $(SRCS:src/%.c=obj/debug/shared/%.o)
@$(call MKDIR,$(@D))
$(CC) -shared -o $@ $^ $(LDFLAGS_PLAT)
obj/debug/%.o: src/%.c
$(BIN_DIR)/debug/$(STATIC_LIB): $(SRCS:src/%.c=obj/debug/static/%.o)
@$(call MKDIR,$(@D))
ar rcs $@ $^
obj/debug/shared/%.o: src/%.c
@$(call MKDIR,$(@D))
$(CC) $(CFLAGS_BASE) -g -O0 -MMD -MP -c $< -o $@
obj/debug/static/%.o: src/%.c
@$(call MKDIR,$(@D))
$(CC) $(CFLAGS_BASE) $(CFLAGS_STATIC) -g -O0 -MMD -MP -c $< -o $@
# Release Rules
$(BIN_DIR)/release/$(FULL_LIB): $(SRCS:src/%.c=obj/release/%.o)
$(BIN_DIR)/release/$(FULL_LIB): $(SRCS:src/%.c=obj/release/shared/%.o)
@$(call MKDIR,$(@D))
$(CC) -shared $(RELEASE_LDFLAGS) -o $@ $^ $(LDFLAGS_PLAT)
obj/release/%.o: src/%.c
$(BIN_DIR)/release/$(STATIC_LIB): $(SRCS:src/%.c=obj/release/static/%.o)
@$(call MKDIR,$(@D))
ar rcs $@ $^
obj/release/shared/%.o: src/%.c
@$(call MKDIR,$(@D))
$(CC) $(CFLAGS_BASE) -O2 -MMD -MP -c $< -o $@
-include $(wildcard obj/debug/*.d)
-include $(wildcard obj/release/*.d)
obj/release/static/%.o: src/%.c
@$(call MKDIR,$(@D))
$(CC) $(CFLAGS_BASE) $(CFLAGS_STATIC) -O2 -MMD -MP -c $< -o $@
-include $(wildcard obj/debug/shared/*.d)
-include $(wildcard obj/debug/static/*.d)
-include $(wildcard obj/release/shared/*.d)
-include $(wildcard obj/release/static/*.d)
clean:
@$(call RMDIR,$(OBJ_DIR))
@@ -58,23 +80,26 @@ test: debug
@echo "=== Building and running stk tests ==="
@$(MAKE) -C test -f gmake.mk
# Installation (Unix only)
ifneq ($(OS),Windows_NT)
install: release
install:
@test -f $(BIN_DIR)/release/$(FULL_LIB) || { echo "Run 'make -f gmake.mk release' before installing."; exit 1; }
install -d $(LIBDIR) $(INCDIR)/stk
install -m 755 $(BIN_DIR)/release/$(FULL_LIB) $(LIBDIR)/
install -m 644 $(BIN_DIR)/release/$(STATIC_LIB) $(LIBDIR)/
install -m 644 $(INC_DIR)/stk.h $(INCDIR)/stk/
install -m 644 $(INC_DIR)/stk_version.h $(INCDIR)/stk/
install -m 644 $(INC_DIR)/stk_log.h $(INCDIR)/stk/
uninstall:
rm -f $(LIBDIR)/$(FULL_LIB)
rm -f $(LIBDIR)/$(STATIC_LIB)
rm -rf $(INCDIR)/stk
else
install:
@echo "make install is not supported on Windows."
@echo "Copy include/ directory contents to your_project/include/stk/"
@echo "Copy bin/release/stk.dll to your project's lib directory."
@echo "Copy bin/release/stk.lib to your project's lib directory."
uninstall:
@echo "make uninstall is not supported on Windows."
+30 -2
View File
@@ -2,12 +2,17 @@
#define STK_H
#include "stk_version.h"
#include <stdint.h>
#include <stdlib.h>
/* Buffers */
#define STK_LOG_PREFIX_BUFFER 64
#define STK_MOD_DEP_OPERATOR_BUFFER 3
#define STK_MOD_DEP_LOG_BUFFER 2048
#define STK_MOD_DESC_BUFFER 256
#define STK_MOD_DIR_BUFFER 256
#define STK_MOD_ID_BUFFER 64
#define STK_MOD_NAME_BUFFER 128
#define STK_MOD_VERSION_BUFFER 32
#define STK_PATH_MAX 256
#define STK_PATH_MAX_OS 4096
@@ -23,6 +28,9 @@
#define STK_MOD_LIBRARY_LOAD_ERROR 2
#define STK_MOD_SYMBOL_NOT_FOUND_ERROR 3
#define STK_MOD_REALLOC_FAILURE 4
#define STK_MOD_DEP_NOT_FOUND_ERROR 5
#define STK_MOD_DEP_VERSION_MISMATCH_ERROR 6
#define STK_MOD_DEP_CIRCULAR_ERROR 7
/* Platform return codes */
#define STK_PLATFORM_OPERATION_SUCCESS 0
@@ -31,6 +39,15 @@
#define STK_PLATFORM_REMOVE_DIR_ERROR 3
#define STK_PLATFORM_REMOVE_FILE_ERROR 4
/* Settings flags */
#define STK_FLAG_INITIALIZED 0x01
#define STK_FLAG_LOGGING_ENABLED 0x02
/* Dependency constraint operators */
#define STK_DEP_MIN 0
#define STK_DEP_EXACT 1
#define STK_DEP_COMPAT 2
#if defined(__linux__) || defined(_WIN32)
#define STK_EVENT_BUFFER 4096
#endif
@@ -55,7 +72,12 @@ typedef enum {
STK_MOD_RELOAD
} stk_module_event_t;
uint8_t stk_init(void);
typedef struct {
char id[STK_MOD_ID_BUFFER];
char version[STK_MOD_VERSION_BUFFER];
} stk_dep_t;
unsigned char stk_init(void);
void stk_shutdown(void);
size_t stk_module_count(void);
size_t stk_poll(void);
@@ -63,6 +85,12 @@ void stk_set_mod_dir(const char *path);
void stk_set_tmp_dir_name(const char *name);
void stk_set_module_init_fn(const char *name);
void stk_set_module_shutdown_fn(const char *name);
void stk_set_logging_enabled(unsigned char enabled);
void stk_set_module_name_fn(const char *name);
void stk_set_module_version_fn(const char *name);
void stk_set_module_description_fn(const char *name);
void stk_set_module_deps_sym(const char *name);
unsigned char stk_is_logging_enabled(void);
#ifdef __cplusplus
}
+12 -1
View File
@@ -7,7 +7,18 @@
extern "C" {
#endif
void stk_log(FILE *fp, const char *fmt, ...);
typedef enum {
STK_LOG_DEBUG,
STK_LOG_INFO,
STK_LOG_WARN,
STK_LOG_ERROR
} stk_log_level_t;
void stk_set_log_output(FILE *fp);
void stk_set_log_prefix(const char *prefix);
void stk_set_log_level(stk_log_level_t min_level);
void stk_log(stk_log_level_t level, const char *fmt, ...);
#ifdef __cplusplus
}
+2 -2
View File
@@ -1,9 +1,9 @@
#ifndef STK_VERSION_H
#define STK_VERSION_H
#define STK_VERSION_MAJOR 0
#define STK_VERSION_MAJOR 1
#define STK_VERSION_MINOR 0
#define STK_VERSION_PATCH 1
#define STK_VERSION_PATCH 0
#define STK_STRINGIFY_HELPER(x) #x
#define STK_STRINGIFY(x) STK_STRINGIFY_HELPER(x)
+981 -111
View File
File diff suppressed because it is too large Load Diff
+166 -118
View File
@@ -1,39 +1,34 @@
#include "stk.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef _WIN32
#include <dirent.h>
#include <dlfcn.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#endif
#if defined(__linux__)
#include <dirent.h>
#include <dlfcn.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <unistd.h>
#elif defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <dirent.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <sys/event.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#endif
int is_mod_loaded(const char *module_name);
uint8_t is_valid_module_file(const char *filename);
unsigned char is_valid_module_file(const char *filename);
void extract_module_id(const char *path, char *out_id);
#ifndef __linux__
static uint8_t is_file_ready(const char *dir_path, const char *filename)
static unsigned char is_file_ready(const char *dir_path, const char *filename)
{
char full_path[STK_PATH_MAX_OS];
#ifdef _WIN32
@@ -80,6 +75,7 @@ static uint8_t is_file_ready(const char *dir_path, const char *filename)
#endif
}
#ifndef __linux__
typedef struct {
char filename[STK_PATH_MAX];
#ifdef _WIN32
@@ -108,12 +104,32 @@ typedef struct {
} platform_watch_context_t;
#endif
uint8_t platform_mkdir(const char *path)
unsigned char platform_mkdir(const char *path)
{
#ifdef _WIN32
return CreateDirectoryA(path, NULL) ? STK_PLATFORM_OPERATION_SUCCESS
: STK_PLATFORM_MKDIR_ERROR;
DWORD attrib;
attrib = GetFileAttributesA(path);
if (attrib != INVALID_FILE_ATTRIBUTES &&
(attrib & FILE_ATTRIBUTE_DIRECTORY))
return STK_PLATFORM_OPERATION_SUCCESS;
if (!CreateDirectoryA(path, NULL))
return STK_PLATFORM_MKDIR_ERROR;
if (strrchr(path, '\\') && *(strrchr(path, '\\') + 1) == '.')
SetFileAttributesA(path, FILE_ATTRIBUTE_HIDDEN);
else if (!strrchr(path, '\\') && path[0] == '.')
SetFileAttributesA(path, FILE_ATTRIBUTE_HIDDEN);
return STK_PLATFORM_OPERATION_SUCCESS;
#else
struct stat st;
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode))
return STK_PLATFORM_OPERATION_SUCCESS;
return mkdir(path, 0755) == 0 ? STK_PLATFORM_OPERATION_SUCCESS
: STK_PLATFORM_MKDIR_ERROR;
#endif
@@ -130,7 +146,7 @@ int platform_remove_file(const char *path)
#endif
}
uint8_t platform_copy_file(const char *from, const char *to)
unsigned char platform_copy_file(const char *from, const char *to)
{
char buf[STK_PATH_MAX_OS];
int ret = STK_PLATFORM_FILE_COPY_ERROR;
@@ -146,7 +162,6 @@ uint8_t platform_copy_file(const char *from, const char *to)
FILE *src = NULL, *dst = NULL;
char tmp_path[STK_PATH_MAX_OS];
size_t n;
#ifndef __linux__
char dir_path[STK_PATH_MAX_OS];
const char *filename;
@@ -163,7 +178,6 @@ uint8_t platform_copy_file(const char *from, const char *to)
if (!is_file_ready(dir_path, filename))
goto done;
#endif
sprintf(tmp_path, "%s.tmp", to);
@@ -205,7 +219,7 @@ done:
return ret;
}
uint8_t platform_remove_dir(const char *path)
unsigned char platform_remove_dir(const char *path)
{
#ifdef _WIN32
WIN32_FIND_DATAA fd;
@@ -264,7 +278,7 @@ void *platform_load_library(const char *path)
#ifdef _WIN32
return (void *)LoadLibraryA(path);
#else
return dlopen(path, RTLD_NOW);
return dlopen(path, RTLD_NOW | RTLD_GLOBAL);
#endif
}
@@ -286,128 +300,127 @@ void *platform_get_symbol(void *h, const char *s)
#endif
}
char (*platform_directory_init_scan(const char *dir_path,
size_t *out_count))[STK_PATH_MAX]
{
size_t count = 0, i = 0, name_len;
char(*list)[STK_PATH_MAX] = NULL;
char (*platform_directory_init_scan(const char *dir_path, size_t *out_count))
[STK_PATH_MAX] {
size_t count = 0, i = 0, name_len;
char (*list)[STK_PATH_MAX] = NULL;
#ifdef _WIN32
WIN32_FIND_DATAA fd;
HANDLE h;
char s[STK_PATH_MAX_OS];
WIN32_FIND_DATAA fd;
HANDLE h;
char s[STK_PATH_MAX_OS];
sprintf(s, "%s\\*", dir_path);
h = FindFirstFileA(s, &fd);
if (h == INVALID_HANDLE_VALUE)
goto create_and_exit;
sprintf(s, "%s\\*", dir_path);
h = FindFirstFileA(s, &fd);
if (h == INVALID_HANDLE_VALUE)
goto create_and_exit;
do {
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
continue;
if (is_valid_module_file(fd.cFileName))
count++;
} while (FindNextFileA(h, &fd));
do {
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
continue;
if (is_valid_module_file(fd.cFileName))
count++;
} while (FindNextFileA(h, &fd));
FindClose(h);
FindClose(h);
if (count == 0)
goto exit;
if (count == 0)
goto exit;
list = malloc(count * sizeof(*list));
if (!list)
goto exit;
list = malloc(count * sizeof(*list));
if (!list)
goto exit;
h = FindFirstFileA(s, &fd);
if (h == INVALID_HANDLE_VALUE)
goto exit;
h = FindFirstFileA(s, &fd);
if (h == INVALID_HANDLE_VALUE)
goto exit;
do {
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
continue;
if (is_valid_module_file(fd.cFileName) && i < count) {
name_len = strlen(fd.cFileName);
if (name_len >= STK_PATH_MAX)
name_len = STK_PATH_MAX - 1;
do {
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
continue;
if (is_valid_module_file(fd.cFileName) && i < count) {
name_len = strlen(fd.cFileName);
if (name_len >= STK_PATH_MAX)
name_len = STK_PATH_MAX - 1;
memcpy(list[i], fd.cFileName, name_len);
list[i][name_len] = '\0';
i++;
}
} while (FindNextFileA(h, &fd));
memcpy(list[i], fd.cFileName, name_len);
list[i][name_len] = '\0';
i++;
}
} while (FindNextFileA(h, &fd));
FindClose(h);
goto exit;
FindClose(h);
goto exit;
create_and_exit:
platform_mkdir(dir_path);
exit:
*out_count = i;
return list;
create_and_exit:
platform_mkdir(dir_path);
exit:
*out_count = i;
return list;
#else
DIR *d;
struct dirent *e;
struct stat st;
char f[STK_PATH_MAX_OS];
DIR *d;
struct dirent *e;
struct stat st;
char f[STK_PATH_MAX_OS];
d = opendir(dir_path);
if (!d)
goto create_and_exit;
d = opendir(dir_path);
if (!d)
goto create_and_exit;
count_loop:
e = readdir(d);
if (!e)
goto count_done;
count_loop:
e = readdir(d);
if (!e)
goto count_done;
sprintf(f, "%s/%s", dir_path, e->d_name);
if (!is_valid_module_file(e->d_name))
goto count_loop;
sprintf(f, "%s/%s", dir_path, e->d_name);
if (!is_valid_module_file(e->d_name))
goto count_loop;
if (stat(f, &st) != 0 || !S_ISREG(st.st_mode))
goto count_loop;
if (stat(f, &st) != 0 || !S_ISREG(st.st_mode))
goto count_loop;
count++;
goto count_loop;
count++;
goto count_loop;
count_done:
if (count == 0)
goto close_and_exit;
count_done:
if (count == 0)
goto close_and_exit;
rewinddir(d);
list = malloc(count * sizeof(*list));
if (!list)
goto close_and_exit;
rewinddir(d);
list = malloc(count * sizeof(*list));
if (!list)
goto close_and_exit;
fill_loop:
e = readdir(d);
if (!e || i >= count)
goto close_and_exit;
fill_loop:
e = readdir(d);
if (!e || i >= count)
goto close_and_exit;
sprintf(f, "%s/%s", dir_path, e->d_name);
if (!is_valid_module_file(e->d_name))
goto fill_loop;
sprintf(f, "%s/%s", dir_path, e->d_name);
if (!is_valid_module_file(e->d_name))
goto fill_loop;
if (stat(f, &st) != 0 || !S_ISREG(st.st_mode))
goto fill_loop;
if (stat(f, &st) != 0 || !S_ISREG(st.st_mode))
goto fill_loop;
name_len = strlen(e->d_name);
if (name_len >= STK_PATH_MAX) {
name_len = STK_PATH_MAX - 1;
}
memcpy(list[i++], e->d_name, name_len);
list[i - 1][name_len] = '\0';
goto fill_loop;
name_len = strlen(e->d_name);
if (name_len >= STK_PATH_MAX) {
name_len = STK_PATH_MAX - 1;
}
memcpy(list[i++], e->d_name, name_len);
list[i - 1][name_len] = '\0';
goto fill_loop;
create_and_exit:
platform_mkdir(dir_path);
*out_count = 0;
return NULL;
create_and_exit:
platform_mkdir(dir_path);
*out_count = 0;
return NULL;
close_and_exit:
closedir(d);
*out_count = i;
return list;
close_and_exit:
closedir(d);
*out_count = i;
return list;
#endif
}
}
#if !defined(__linux__) && !defined(_WIN32)
static void update_watches(platform_watch_context_t *ctx)
@@ -665,7 +678,7 @@ stk_module_event_t *platform_directory_watch_check(
int fd = (int)(long)handle;
char buf[STK_EVENT_BUFFER];
ssize_t len;
size_t index = 0, count = 0, i;
size_t index = 0, count = 0, i, write_index;
stk_module_event_t *evs;
char *ptr, *end;
struct inotify_event *e;
@@ -763,6 +776,19 @@ stk_module_event_t *platform_directory_watch_check(
}
}
write_index = 0;
for (i = 0; i < index; ++i) {
if (evs[i] != -1) {
if (write_index != i) {
evs[write_index] = evs[i];
memmove((*file_list)[write_index],
(*file_list)[i], STK_PATH_MAX);
}
write_index++;
}
}
index = write_index;
*out_count = index;
return evs;
@@ -1000,3 +1026,25 @@ no_change:
return NULL;
#endif
}
void platform_get_timestamp(char *buffer, size_t size)
{
#ifdef _WIN32
SYSTEMTIME st;
GetLocalTime(&st);
sprintf(buffer, "%04d-%02d-%02d %02d:%02d:%02d.%03d", st.wYear,
st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond,
st.wMilliseconds);
#else
struct timeval tv;
struct tm *tm_info;
gettimeofday(&tv, NULL);
tm_info = localtime(&tv.tv_sec);
sprintf(buffer, "%04d-%02d-%02d %02d:%02d:%02d.%03d",
tm_info->tm_year + 1900, tm_info->tm_mon + 1, tm_info->tm_mday,
tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec,
(int)(tv.tv_usec / 1000));
#endif
}
+436 -123
View File
@@ -1,7 +1,6 @@
#include "stk.h"
#include "platform.h"
#include "stk_log.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -9,18 +8,26 @@
typedef int (*stk_init_mod_func)(void);
typedef void (*stk_shutdown_mod_func)(void);
extern void **stk_handles;
extern stk_init_mod_func *stk_inits;
extern stk_shutdown_mod_func *stk_shutdowns;
extern char (*stk_module_ids)[STK_MOD_ID_BUFFER];
typedef struct {
char desc[STK_MOD_DESC_BUFFER];
char name[STK_MOD_NAME_BUFFER];
char id[STK_MOD_ID_BUFFER];
char version[STK_MOD_VERSION_BUFFER];
void *handle;
stk_init_mod_func init;
stk_shutdown_mod_func shutdown;
stk_dep_t *deps;
size_t dep_count;
} stk_mod_t;
extern stk_mod_t *stk_modules;
extern size_t module_count;
uint8_t stk_initialized = 0;
unsigned char stk_flags = STK_FLAG_LOGGING_ENABLED;
static char stk_mod_dir[STK_PATH_MAX_OS] = "mods";
static char stk_tmp_name[STK_MOD_ID_BUFFER] = ".tmp";
static char stk_tmp_dir[STK_PATH_MAX_OS] = "mods/.tmp";
static char stk_tmp_dir[STK_PATH_MAX_OS] = "";
static void *watch_handle = NULL;
char (*platform_directory_init_scan(const char *path,
@@ -30,20 +37,35 @@ void platform_directory_watch_stop(void *handle);
stk_module_event_t *platform_directory_watch_check(
void *handle, char (**file_list)[STK_PATH_MAX], size_t *out_count,
char (*loaded_module_ids)[STK_MOD_ID_BUFFER], const size_t loaded_count);
uint8_t platform_mkdir(const char *path);
uint8_t platform_copy_file(const char *from, const char *to);
uint8_t platform_remove_dir(const char *path);
unsigned char platform_mkdir(const char *path);
unsigned char platform_copy_file(const char *from, const char *to);
unsigned char platform_remove_dir(const char *path);
void extract_module_id(const char *path, char *out_id);
int is_mod_loaded(const char *module_id);
size_t stk_module_count(void);
uint8_t stk_module_load(const char *path, int index);
uint8_t stk_module_load_init(const char *path, int index);
uint8_t stk_module_init_memory(size_t capacity);
uint8_t stk_module_realloc_memory(size_t new_capacity);
unsigned char stk_module_preload(const char *path, int index);
unsigned char stk_module_activate(size_t index);
unsigned char stk_validate_dependencies_single(size_t index);
void stk_log_dependency_failures(size_t index, const char *action);
void stk_module_discard(size_t index);
unsigned char stk_module_load(const char *path, int index);
unsigned char stk_module_load_init(const char *path, int index);
unsigned char stk_module_init_memory(size_t capacity);
unsigned char stk_module_realloc_memory(size_t new_capacity);
void stk_module_unload(size_t index);
void stk_module_unload_all(void);
unsigned char stk_validate_dependencies(size_t count);
unsigned char stk_topo_sort(size_t count, size_t *order);
void stk_pending_add(const char *path);
void stk_pending_add_batch(const char (*paths)[STK_PATH_MAX_OS], size_t count);
void stk_pending_remove(const char *id);
size_t stk_pending_retry(void);
void stk_sort_unload_order(size_t *indices, size_t n);
void stk_collect_dependents(size_t *indices, size_t *count, size_t capacity);
void stk_sort_load_order(int *file_indices, size_t n,
char (*file_names)[STK_PATH_MAX], const char *tmp_dir);
static void build_path(char *dest, size_t dest_size, const char *dir,
const char *file)
@@ -65,29 +87,71 @@ static const char *stk_error_string(int error_code)
return "init failure";
case STK_MOD_REALLOC_FAILURE:
return "memory reallocation failed";
case STK_MOD_DEP_NOT_FOUND_ERROR:
return "dependency not found";
case STK_MOD_DEP_VERSION_MISMATCH_ERROR:
return "dependency version mismatch";
case STK_MOD_DEP_CIRCULAR_ERROR:
return "circular dependency detected";
default:
return "unknown error";
}
}
uint8_t stk_init(void)
static void stk_log_module(size_t index)
{
const char *name =
stk_modules[index].name[0] ? stk_modules[index].name : NULL;
const char *desc =
stk_modules[index].desc[0] ? stk_modules[index].desc : NULL;
if (name && desc)
stk_log(STK_LOG_INFO, " %s v%s - %s (%s)",
stk_modules[index].id, stk_modules[index].version, desc,
name);
else if (name)
stk_log(STK_LOG_INFO, " %s v%s (%s)", stk_modules[index].id,
stk_modules[index].version, name);
else if (desc)
stk_log(STK_LOG_INFO, " %s v%s - %s", stk_modules[index].id,
stk_modules[index].version, desc);
else
stk_log(STK_LOG_INFO, " %s v%s", stk_modules[index].id,
stk_modules[index].version);
}
static void stk_log_modules(void)
{
size_t i;
stk_log(STK_LOG_INFO,
"Loaded modules (%lu):", (unsigned long)module_count);
for (i = 0; i < module_count; i++)
stk_log_module(i);
}
unsigned char stk_init(void)
{
char (*files)[STK_PATH_MAX] = NULL;
size_t file_count, i, successful_loads = 0;
char (*test_scan)[STK_PATH_MAX];
size_t file_count, i, j, write, successful_loads = 0;
size_t index, test_count;
char full_path[STK_PATH_MAX_OS];
char tmp_path[STK_PATH_MAX_OS];
int load_result;
unsigned char dep_result;
size_t *order = NULL;
char (*init_batch)[STK_PATH_MAX_OS] = NULL;
size_t init_batch_count = 0;
platform_mkdir(stk_mod_dir);
build_path(stk_tmp_dir, sizeof(stk_tmp_dir), stk_mod_dir, stk_tmp_name);
if (platform_mkdir(stk_tmp_dir) != STK_PLATFORM_OPERATION_SUCCESS) {
char (*test_scan)[STK_PATH_MAX];
size_t test_count;
test_scan =
platform_directory_init_scan(stk_tmp_dir, &test_count);
if (test_scan)
free(test_scan);
if (!test_scan && test_count == 0) {
stk_log(stderr,
stk_log(STK_LOG_ERROR,
"FATAL: Cannot create temp directory: %s",
stk_tmp_dir);
return STK_INIT_TMPDIR_ERROR;
@@ -97,7 +161,7 @@ uint8_t stk_init(void)
files = platform_directory_init_scan(stk_mod_dir, &file_count);
if (file_count > 0 && stk_module_init_memory(file_count) != 0) {
stk_log(stderr, "FATAL: Memory allocation failed");
stk_log(STK_LOG_ERROR, "FATAL: Memory allocation failed");
return STK_INIT_MEMORY_ERROR;
}
@@ -110,41 +174,110 @@ uint8_t stk_init(void)
if (platform_copy_file(full_path, tmp_path) !=
STK_PLATFORM_OPERATION_SUCCESS) {
stk_log(stderr, "Failed to copy %s to temp directory",
stk_log(STK_LOG_ERROR,
"Failed to copy %s to temp directory",
files[i]);
continue;
}
load_result = stk_module_load_init(tmp_path, successful_loads);
load_result = stk_module_preload(tmp_path, successful_loads);
if (load_result != STK_MOD_INIT_SUCCESS) {
stk_log(stderr, "Failed to load module %s: %s",
files[i], stk_error_string(load_result));
stk_log(STK_LOG_ERROR,
"Failed to preload module %s: %s", files[i],
stk_error_string(load_result));
} else {
successful_loads++;
module_count++;
}
}
if (successful_loads < file_count) {
if (successful_loads < file_count)
stk_module_realloc_memory(successful_loads);
}
free(files);
if (module_count == 0)
goto scanned;
order = malloc(module_count * sizeof(size_t));
if (order) {
dep_result = stk_topo_sort(module_count, order);
if (dep_result != STK_MOD_INIT_SUCCESS)
stk_log(STK_LOG_ERROR, "Dependency sort failed: %s",
stk_error_string(dep_result));
}
init_batch = malloc(module_count * sizeof(*init_batch));
for (j = 0; j < module_count; j++) {
index = order ? order[j] : j;
dep_result = stk_validate_dependencies_single(index);
if (dep_result != STK_MOD_INIT_SUCCESS) {
stk_log_dependency_failures(index, "Deferring");
if (init_batch) {
build_path(init_batch[init_batch_count],
sizeof(init_batch[init_batch_count]),
stk_tmp_dir, stk_modules[index].id);
strncat(
init_batch[init_batch_count],
STK_MODULE_EXT,
sizeof(init_batch[init_batch_count]) -
strlen(init_batch[init_batch_count]) -
1);
init_batch_count++;
}
stk_module_discard(index);
continue;
}
if (stk_module_activate(index) != STK_MOD_INIT_SUCCESS) {
stk_log(STK_LOG_ERROR, "Failed to init module %s",
stk_modules[index].id);
stk_module_discard(index);
}
}
if (init_batch_count > 0)
stk_pending_add_batch(
(const char (*)[STK_PATH_MAX_OS])init_batch,
init_batch_count);
free(init_batch);
init_batch = NULL;
if (order) {
free(order);
order = NULL;
}
write = 0;
for (j = 0; j < module_count; j++) {
if (stk_modules[j].handle != NULL) {
if (write != j)
stk_modules[write] = stk_modules[j];
write++;
}
}
module_count = write;
scanned:
watch_handle = platform_directory_watch_start(stk_mod_dir);
if (!watch_handle) {
stk_log(stderr, "FATAL: Cannot start directory watch on %s",
stk_log(STK_LOG_ERROR,
"FATAL: Cannot start directory watch on %s",
stk_mod_dir);
stk_module_unload_all();
return STK_INIT_WATCH_ERROR;
}
stk_log(stdout, "stk v%s initialized! Loaded %lu mod%s from %s/",
STK_VERSION_STRING, module_count, module_count != 1 ? "s" : "",
stk_mod_dir);
stk_pending_retry();
stk_initialized = 1;
stk_log(STK_LOG_INFO, "stk v%s initialized, watching %s/",
STK_VERSION_STRING, stk_mod_dir);
if (module_count > 0)
stk_log_modules();
stk_flags |= STK_FLAG_INITIALIZED;
return STK_INIT_SUCCESS;
}
@@ -159,12 +292,13 @@ void stk_shutdown(void)
if (platform_remove_dir(stk_tmp_dir) !=
STK_PLATFORM_OPERATION_SUCCESS) {
stk_log(stderr, "Warning: failed to remove temp directory %s",
stk_log(STK_LOG_WARN,
"Warning: failed to remove temp directory %s",
stk_tmp_dir);
}
stk_initialized = 0;
stk_log(stdout, "stk shutdown");
stk_flags &= ~STK_FLAG_INITIALIZED;
stk_log(STK_LOG_INFO, "stk shutdown");
}
size_t stk_poll(void)
@@ -172,20 +306,50 @@ size_t stk_poll(void)
char (*file_list)[STK_PATH_MAX] = NULL;
stk_module_event_t *events = NULL;
size_t i, file_count = 0, reload_count = 0, load_count = 0,
unload_count = 0, reload_index = 0, load_index = 0,
unload_index = 0;
unload_count = 0;
int *reloaded_mod_indices = NULL, *reloaded_mod_file_indices = NULL,
*unloaded_mod_indices = NULL, *loaded_mod_indices = NULL;
size_t remaining_loads, new_capacity, holes_to_fill;
size_t write_pos, read_pos;
size_t new_capacity;
char full_path[STK_PATH_MAX_OS], tmp_path[STK_PATH_MAX_OS];
char mod_id[STK_MOD_ID_BUFFER];
int load_result;
size_t successful_appends = 0;
char (*module_ids)[STK_MOD_ID_BUFFER] = NULL;
unsigned char dep_result;
size_t *order = NULL;
size_t *unload_order = NULL;
size_t expanded_count;
size_t index, oi;
int is_orig;
size_t write;
int file_index, mod_index;
size_t *cascade_indices = NULL;
size_t cascade_count;
size_t j, k, cascade_write;
char (*dep_batch)[STK_PATH_MAX_OS] = NULL;
size_t dep_batch_count = 0;
char (*cascade_batch)[STK_PATH_MAX_OS] = NULL;
size_t cascade_batch_count = 0;
char (*load_batch)[STK_PATH_MAX_OS] = NULL;
size_t load_batch_count = 0;
if (module_count > 0) {
module_ids = malloc(module_count * sizeof(*module_ids));
if (module_ids) {
for (i = 0; i < module_count; i++) {
strncpy(module_ids[i], stk_modules[i].id,
STK_MOD_ID_BUFFER - 1);
module_ids[i][STK_MOD_ID_BUFFER - 1] = '\0';
}
}
}
events = platform_directory_watch_check(
watch_handle, &file_list, &file_count, module_ids, module_count);
if (module_ids)
free(module_ids);
events = platform_directory_watch_check(watch_handle, &file_list,
&file_count, stk_module_ids,
module_count);
if (!events)
goto finish_poll;
@@ -208,20 +372,31 @@ size_t stk_poll(void)
unloaded_mod_indices = malloc(unload_count * sizeof(int));
loaded_mod_indices = malloc(load_count * sizeof(int));
reload_count = 0;
unload_count = 0;
load_count = 0;
for (i = 0; i < file_count; ++i) {
int mod_index;
extract_module_id(file_list[i], mod_id);
switch (events[i]) {
case STK_MOD_LOAD:
loaded_mod_indices[load_index++] = i;
loaded_mod_indices[load_count++] = i;
break;
case STK_MOD_RELOAD:
reloaded_mod_file_indices[reload_index] = i;
reloaded_mod_indices[reload_index++] =
is_mod_loaded(mod_id);
mod_index = is_mod_loaded(mod_id);
if (mod_index >= 0) {
reloaded_mod_file_indices[reload_count] = i;
reloaded_mod_indices[reload_count] = mod_index;
reload_count++;
}
break;
case STK_MOD_UNLOAD:
unloaded_mod_indices[unload_index++] =
is_mod_loaded(mod_id);
mod_index = is_mod_loaded(mod_id);
if (mod_index >= 0) {
unloaded_mod_indices[unload_count] = mod_index;
unload_count++;
}
break;
}
}
@@ -232,96 +407,142 @@ size_t stk_poll(void)
goto begin_operations;
handle_grow:
remaining_loads = load_count - unload_count;
new_capacity = module_count + remaining_loads;
if (stk_module_realloc_memory(new_capacity) != STK_MOD_INIT_SUCCESS) {
new_capacity = module_count + load_count;
if (stk_module_realloc_memory(new_capacity) != STK_MOD_INIT_SUCCESS)
goto free_poll;
}
begin_operations:
for (i = 0; i < unload_count; ++i)
stk_module_unload(unloaded_mod_indices[i]);
unload_order = malloc(module_count * sizeof(size_t));
if (unload_order) {
expanded_count = unload_count;
for (i = 0; i < reload_count; ++i)
stk_module_unload(reloaded_mod_indices[i]);
for (i = 0; i < unload_count; i++)
unload_order[i] = (size_t)unloaded_mod_indices[i];
stk_collect_dependents(unload_order, &expanded_count,
module_count);
stk_sort_unload_order(unload_order, expanded_count);
dep_batch = malloc(expanded_count * sizeof(*dep_batch));
dep_batch_count = 0;
for (i = 0; i < expanded_count; i++) {
index = unload_order[i];
stk_log(STK_LOG_INFO, "Unloaded module: %s",
stk_modules[index].id);
stk_pending_remove(stk_modules[index].id);
is_orig = 0;
for (oi = 0; oi < unload_count; oi++) {
if ((size_t)unloaded_mod_indices[oi] == index) {
is_orig = 1;
break;
}
}
if (!is_orig && dep_batch) {
build_path(dep_batch[dep_batch_count],
sizeof(dep_batch[dep_batch_count]),
stk_tmp_dir, stk_modules[index].id);
strncat(
dep_batch[dep_batch_count], STK_MODULE_EXT,
sizeof(dep_batch[dep_batch_count]) -
strlen(dep_batch[dep_batch_count]) - 1);
dep_batch_count++;
}
stk_module_unload(index);
}
if (dep_batch_count > 0)
stk_pending_add_batch(
(const char (*)[STK_PATH_MAX_OS])dep_batch,
dep_batch_count);
free(dep_batch);
dep_batch = NULL;
free(unload_order);
unload_order = NULL;
} else {
for (i = 0; i < unload_count; ++i) {
stk_log(STK_LOG_INFO, "Unloaded module: %s",
stk_modules[unloaded_mod_indices[i]].id);
stk_pending_remove(
stk_modules[unloaded_mod_indices[i]].id);
stk_module_unload(unloaded_mod_indices[i]);
}
}
if (unload_count > 0) {
write = 0;
for (i = 0; i < module_count; i++) {
if (stk_modules[i].handle != NULL) {
if (write != i)
stk_modules[write] = stk_modules[i];
write++;
}
}
module_count = write;
if (module_count > 0)
stk_module_realloc_memory(module_count);
}
for (i = 0; i < reload_count; ++i) {
int file_index = reloaded_mod_file_indices[i];
int mod_index = reloaded_mod_indices[i];
file_index = reloaded_mod_file_indices[i];
mod_index = reloaded_mod_indices[i];
build_path(full_path, sizeof(full_path), stk_mod_dir,
file_list[file_index]);
build_path(tmp_path, sizeof(tmp_path), stk_tmp_dir,
file_list[file_index]);
stk_module_unload(mod_index);
if (platform_copy_file(full_path, tmp_path) !=
STK_PLATFORM_OPERATION_SUCCESS) {
stk_log(stderr, "Failed to copy %s for reload",
stk_log(STK_LOG_ERROR, "Failed to copy %s for reload",
file_list[file_index]);
continue;
}
load_result = stk_module_load(tmp_path, mod_index);
if (load_result != STK_MOD_INIT_SUCCESS) {
stk_log(stderr, "Failed to reload module %s: %s",
if (load_result != STK_MOD_INIT_SUCCESS)
stk_log(STK_LOG_ERROR, "Failed to reload module %s: %s",
file_list[file_index],
stk_error_string(load_result));
}
}
holes_to_fill = (load_count < unload_count) ? load_count : unload_count;
for (i = 0; i < holes_to_fill; ++i) {
int target_index = unloaded_mod_indices[i];
int file_index = loaded_mod_indices[i];
for (i = 0; i < load_count; i++) {
file_index = loaded_mod_indices[i];
build_path(full_path, sizeof(full_path), stk_mod_dir,
file_list[file_index]);
build_path(tmp_path, sizeof(tmp_path), stk_tmp_dir,
file_list[file_index]);
if (platform_copy_file(full_path, tmp_path) !=
STK_PLATFORM_OPERATION_SUCCESS) {
stk_log(stderr, "Failed to copy %s for loading",
file_list[file_index]);
continue;
}
load_result = stk_module_load(tmp_path, target_index);
if (load_result != STK_MOD_INIT_SUCCESS) {
stk_log(stderr, "Failed to load module %s: %s",
file_list[file_index],
stk_error_string(load_result));
}
platform_copy_file(full_path, tmp_path);
}
if (load_count > unload_count)
goto append_modules;
if (load_count > 1)
stk_sort_load_order(loaded_mod_indices, load_count, file_list,
stk_tmp_dir);
if (unload_count > load_count)
goto trim_arrays;
load_batch = malloc(load_count * sizeof(*load_batch));
load_batch_count = 0;
goto free_poll;
for (i = 0; i < load_count; ++i) {
file_index = loaded_mod_indices[i];
append_modules:
for (; i < load_count; ++i) {
int file_index = loaded_mod_indices[i];
build_path(full_path, sizeof(full_path), stk_mod_dir,
file_list[file_index]);
build_path(tmp_path, sizeof(tmp_path), stk_tmp_dir,
file_list[file_index]);
if (platform_copy_file(full_path, tmp_path) !=
STK_PLATFORM_OPERATION_SUCCESS) {
stk_log(stderr, "Failed to copy %s for loading",
file_list[file_index]);
continue;
}
load_result = stk_module_load(tmp_path, module_count +
successful_appends);
if (load_result != STK_MOD_INIT_SUCCESS) {
stk_log(stderr, "Failed to load module %s: %s",
if (load_result == STK_MOD_DEP_NOT_FOUND_ERROR ||
load_result == STK_MOD_DEP_VERSION_MISMATCH_ERROR) {
if (load_batch)
memcpy(load_batch[load_batch_count++], tmp_path,
STK_PATH_MAX_OS);
} else if (load_result != STK_MOD_INIT_SUCCESS) {
stk_log(STK_LOG_ERROR, "Failed to load module %s: %s",
file_list[file_index],
stk_error_string(load_result));
} else {
@@ -331,34 +552,113 @@ append_modules:
module_count += successful_appends;
if (successful_appends < (load_count - holes_to_fill)) {
if (successful_appends < load_count)
stk_module_realloc_memory(module_count);
}
goto free_poll;
if (load_batch_count > 0)
stk_pending_add_batch(
(const char (*)[STK_PATH_MAX_OS])load_batch,
load_batch_count);
trim_arrays:
write_pos = unloaded_mod_indices[holes_to_fill];
for (i = holes_to_fill + 1; i < unload_count; ++i) {
if (unloaded_mod_indices[i] < write_pos)
write_pos = unloaded_mod_indices[i];
}
free(load_batch);
load_batch = NULL;
for (read_pos = write_pos + 1; read_pos < module_count; ++read_pos) {
if (stk_handles[read_pos] != NULL) {
stk_handles[write_pos] = stk_handles[read_pos];
stk_inits[write_pos] = stk_inits[read_pos];
stk_shutdowns[write_pos] = stk_shutdowns[read_pos];
memcpy(stk_module_ids[write_pos],
stk_module_ids[read_pos], STK_MOD_ID_BUFFER);
++write_pos;
goto validate_deps;
validate_deps:
if (module_count == 0)
goto free_poll;
do {
cascade_count = 0;
cascade_indices = malloc(module_count * sizeof(size_t));
if (!cascade_indices)
break;
for (j = 0; j < module_count; j++) {
if (stk_modules[j].dep_count == 0)
continue;
for (k = 0; k < stk_modules[j].dep_count; k++) {
if (is_mod_loaded(stk_modules[j].deps[k].id) <
0) {
cascade_indices[cascade_count++] = j;
break;
}
}
}
}
module_count = write_pos;
stk_module_realloc_memory(module_count);
if (cascade_count == 0) {
free(cascade_indices);
cascade_indices = NULL;
break;
}
cascade_batch = malloc(cascade_count * sizeof(*cascade_batch));
cascade_batch_count = 0;
for (j = 0; j < cascade_count; j++) {
index = cascade_indices[j];
stk_log_dependency_failures(index, "Unloading");
if (cascade_batch) {
build_path(
cascade_batch[cascade_batch_count],
sizeof(cascade_batch[cascade_batch_count]),
stk_tmp_dir, stk_modules[index].id);
strncat(
cascade_batch[cascade_batch_count],
STK_MODULE_EXT,
sizeof(cascade_batch[cascade_batch_count]) -
strlen(cascade_batch
[cascade_batch_count]) -
1);
cascade_batch_count++;
}
stk_module_unload(index);
}
if (cascade_batch_count > 0)
stk_pending_add_batch(
(const char (*)[STK_PATH_MAX_OS])cascade_batch,
cascade_batch_count);
free(cascade_batch);
cascade_batch = NULL;
cascade_write = 0;
for (j = 0; j < module_count; j++) {
if (stk_modules[j].handle != NULL) {
if (cascade_write != j)
stk_modules[cascade_write] =
stk_modules[j];
cascade_write++;
}
}
module_count = cascade_write;
free(cascade_indices);
cascade_indices = NULL;
} while (cascade_count > 0);
if (module_count > 0)
stk_module_realloc_memory(module_count);
order = malloc(module_count * sizeof(size_t));
if (order) {
dep_result = stk_topo_sort(module_count, order);
if (dep_result != STK_MOD_INIT_SUCCESS)
stk_log(STK_LOG_ERROR, "Dependency sort failed: %s",
stk_error_string(dep_result));
free(order);
}
free_poll:
stk_pending_retry();
if (module_count > 0)
stk_log_modules();
free(reloaded_mod_indices);
free(reloaded_mod_file_indices);
free(unloaded_mod_indices);
@@ -372,7 +672,7 @@ finish_poll:
void stk_set_mod_dir(const char *path)
{
if (!path || stk_initialized)
if (!path || (stk_flags & STK_FLAG_INITIALIZED))
return;
strncpy(stk_mod_dir, path, STK_PATH_MAX_OS - 1);
@@ -390,7 +690,7 @@ void stk_set_mod_dir(const char *path)
void stk_set_tmp_dir_name(const char *name)
{
if (!name || stk_initialized)
if (!name || (stk_flags & STK_FLAG_INITIALIZED))
return;
strncpy(stk_tmp_name, name, STK_MOD_ID_BUFFER - 1);
@@ -403,3 +703,16 @@ void stk_set_tmp_dir_name(const char *name)
strncat(stk_tmp_dir, stk_tmp_name,
STK_PATH_MAX_OS - strlen(stk_tmp_dir) - 1);
}
void stk_set_logging_enabled(unsigned char enabled)
{
if (enabled)
stk_flags |= STK_FLAG_LOGGING_ENABLED;
else
stk_flags &= ~STK_FLAG_LOGGING_ENABLED;
}
unsigned char stk_is_logging_enabled(void)
{
return (stk_flags & STK_FLAG_LOGGING_ENABLED) != 0;
}
+77 -5
View File
@@ -1,14 +1,86 @@
#include "stk_log.h"
#include "stk.h"
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
void stk_log(FILE *fp, const char *fmt, ...)
#define STK_LOG_TIMESTAMP_BUFFER 32
extern unsigned char stk_flags;
void platform_get_timestamp(char *buffer, size_t size);
static FILE *log_output = NULL;
static char log_prefix[STK_LOG_PREFIX_BUFFER] = "stk";
static stk_log_level_t min_log_level = STK_LOG_INFO;
static const char *get_level_string(stk_log_level_t level)
{
va_list args;
va_start(args, fmt);
char *level_str = "";
switch (level) {
case STK_LOG_ERROR:
level_str = "ERROR";
break;
case STK_LOG_WARN:
level_str = "WARN";
break;
case STK_LOG_INFO:
level_str = "INFO";
break;
case STK_LOG_DEBUG:
level_str = "DEBUG";
break;
}
vfprintf(fp, fmt, args);
fputc('\n', fp);
return level_str;
}
void stk_set_log_output(FILE *fp)
{
if (fp == NULL)
stk_flags &= ~STK_FLAG_LOGGING_ENABLED;
log_output = fp;
}
void stk_set_log_prefix(const char *prefix)
{
if (!prefix) {
log_prefix[0] = '\0';
return;
}
strncpy(log_prefix, prefix, STK_LOG_PREFIX_BUFFER - 1);
log_prefix[STK_LOG_PREFIX_BUFFER - 1] = '\0';
}
void stk_set_log_level(stk_log_level_t level) { min_log_level = level; }
void stk_log(stk_log_level_t level, const char *fmt, ...)
{
FILE *output;
const char *level_str;
char timestamp[STK_LOG_TIMESTAMP_BUFFER];
va_list args;
if (!(stk_flags & STK_FLAG_LOGGING_ENABLED))
return;
if (level < min_log_level)
return;
output = log_output ? log_output : stdout;
level_str = get_level_string(level);
platform_get_timestamp(timestamp, sizeof(timestamp));
if (log_prefix[0] != '\0')
fprintf(output, "%s [%s] [%s] ", timestamp, log_prefix,
level_str);
else
fprintf(output, "%s [%s] ", timestamp, level_str);
va_start(args, fmt);
vfprintf(output, fmt, args);
va_end(args);
fputc('\n', output);
}
+9 -5
View File
@@ -22,23 +22,27 @@ test_program: test.c
test_mod$(MODULE_EXT): test_mod.c
$(CC) $(CFLAGS) -fPIC -shared -o $@ test_mod.c
test_mod_dep$(MODULE_EXT): test_mod_dep.c
$(CC) $(CFLAGS) -fPIC -shared -o $@ test_mod_dep.c
setup:
@mkdir -p mods
@cp -f test_mod$(MODULE_EXT) mods/ 2>/dev/null || true
@echo "Test environment ready: mods/ directory with test_mod$(MODULE_EXT)"
@cp -f test_mod_dep$(MODULE_EXT) mods/ 2>/dev/null || true
@echo "Test environment ready: mods/ directory with test modules"
run: test_program test_mod$(MODULE_EXT) setup
run: test_program test_mod$(MODULE_EXT) test_mod_dep$(MODULE_EXT) setup
@echo "Running integration test (CTRL+C to exit)..."
@./test_program
test: test_program test_mod$(MODULE_EXT) setup
test: test_program test_mod$(MODULE_EXT) test_mod_dep$(MODULE_EXT) setup
@echo "=== stk Integration Test ==="
@echo "1. Starting test program"
@echo "2. Will load test_mod$(MODULE_EXT) from mods/"
@echo "2. Will load test_mod and test_mod_dep from mods/"
@echo "3. Press CTRL+C to exit"
@echo "============================="
@./test_program || echo "Test completed."
clean:
rm -f test_program test_mod$(MODULE_EXT)
rm -f test_program test_mod$(MODULE_EXT) test_mod_dep$(MODULE_EXT)
rm -rf mods/
+10 -3
View File
@@ -3,6 +3,8 @@ CFLAGS = -Wall -Wpedantic -I../include -std=c89
LDFLAGS = -L../bin/debug -lstk
ifeq ($(OS),Windows_NT)
SHELL := cmd.exe
.SHELLFLAGS := /c
MODULE_EXT = .dll
EXE_EXT = .exe
else
@@ -21,16 +23,21 @@ test_program$(EXE_EXT): test.c
test_mod$(MODULE_EXT): test_mod.c
$(CC) $(CFLAGS) -fPIC -shared -o $@ test_mod.c
test_mod_dep$(MODULE_EXT): test_mod_dep.c
$(CC) $(CFLAGS) -fPIC -shared -o $@ test_mod_dep.c
setup:
ifeq ($(OS),Windows_NT)
@if not exist mods mkdir mods
@if exist test_mod.dll copy /Y test_mod.dll mods\ >nul 2>&1
@if exist test_mod_dep.dll copy /Y test_mod_dep.dll mods\ >nul 2>&1
else
@mkdir -p mods
@cp -f test_mod.so mods/ 2>/dev/null || true
@cp -f test_mod_dep.so mods/ 2>/dev/null || true
endif
test: test_program$(EXE_EXT) test_mod$(MODULE_EXT) setup
test: test_program$(EXE_EXT) test_mod$(MODULE_EXT) test_mod_dep$(MODULE_EXT) setup
ifeq ($(OS),Windows_NT)
@set PATH=../bin/debug;%PATH% && cmd /C "test_program.exe"
else
@@ -39,9 +46,9 @@ endif
clean:
ifeq ($(OS),Windows_NT)
@del /Q test_program.exe test_mod.dll 2>nul || true
@del /Q test_program.exe test_mod.dll test_mod_dep.dll 2>nul || true
@rmdir /S /Q mods 2>nul || true
else
@rm -f test_program test_mod.so
@rm -f test_program test_mod.so test_mod_dep.so
@rm -rf mods
endif
+5 -4
View File
@@ -31,8 +31,8 @@ void inthand(int signum)
int main(int argc, char **argv)
{
uint8_t init_result;
uint64_t iterations = 0;
unsigned char init_result;
size_t iterations = 0;
printf("stk test - CTRL+C to exit\n");
@@ -55,12 +55,13 @@ int main(int argc, char **argv)
while (!stop) {
size_t events = stk_poll();
if (events > 0)
printf("Poll: %lu module event(s) detected\n", (unsigned long) events);
printf("Poll: %lu module event(s) detected\n",
(unsigned long)events);
iterations++;
if (iterations % 5 == 0) {
printf("Still running... (iteration %lu)\n",
(unsigned long) iterations);
(unsigned long)iterations);
}
#ifdef _WIN32
Sleep(1000);
+5 -2
View File
@@ -2,8 +2,11 @@
int stk_mod_init(void)
{
printf("test mod initialized!\n");
printf("test_mod initialized!\n");
return 0;
}
void stk_mod_shutdown(void) { printf("test mod shut down.\n"); }
void stk_mod_shutdown(void) { printf("test_mod shut down.\n"); }
const char *stk_mod_name(void) { return "Test Module"; }
const char *stk_mod_version(void) { return "1.0.0"; }
+19
View File
@@ -0,0 +1,19 @@
#include <stdio.h>
typedef struct {
char id[64];
char version[32];
} dep_t;
int stk_mod_init(void)
{
printf("test_mod_dep initialized!\n");
return 0;
}
void stk_mod_shutdown(void) { printf("test_mod_dep shut down.\n"); }
const char *stk_mod_name(void) { return "Dependent Test Module"; }
const char *stk_mod_version(void) { return "1.0.0"; }
dep_t stk_mod_deps[] = {{"test_mod", ">=1.0.0"}, {"", ""}};