This document explains the core architecture and module system that underlies LibJuno's design.
Overview
LibJuno is built around a lightweight module system that enables dependency injection (DI) in C11. This system allows developers to write decoupled, testable, and portable embedded software without relying on dynamic memory allocation or complex frameworks.
Core Concepts
1. Modules
A module in LibJuno consists of:
- Module Root (
JUNO_MODULE_ROOT): Common, freestanding members shared by all implementations
- Contains the API vtable pointer (
ptApi)
- Contains failure handler callback and user data
- Should avoid hosted dependencies for portability
- Derived Implementations (
JUNO_MODULE_DERIVE): Concrete implementations that embed the root
- First member is always the root (accessible via
JUNO_MODULE_SUPER)
- Add implementation-specific state and dependencies
- Enable safe up-casting to the root type
- Module Union (
JUNO_MODULE): Union of root and all derivations
- Enables safe type punning and polymorphism
- All variants share the same API interface
2. API Vtables
APIs are defined as function pointer tables (vtables) that operate on the module root type:
typedef struct MY_API_T {
JUNO_RESULT_T (*Query)(const MY_ROOT_T *ptModule, ...);
} MY_API_T;
int32_t JUNO_STATUS_T
Canonical status type for LibJuno functions.
Definition status.h:49
This pattern enables:
- Runtime polymorphism: Different implementations behind the same interface
- Dependency injection: Modules receive their dependencies as API pointers
- Testability: Mock implementations can be injected for testing
3. Dependency Injection
Instead of modules creating their own dependencies, they receive them:
MY_MODULE_T *ptModule,
);
struct JUNO_TIME_ROOT_TAG JUNO_TIME_ROOT_T
Definition time_api.h:69
struct JUNO_LOG_ROOT_TAG JUNO_LOG_ROOT_T
Definition log_api.h:49
Benefits:
- No hidden dependencies: All requirements are explicit in function signatures
- Easy to test: Inject mocks/stubs instead of real implementations
- Flexible: Swap implementations at runtime or compile time
- Portable: No global state or singleton patterns
Module System Directory Structure
include/juno/
├── module.h # Core module system macros
├── status.h # Status codes and error handling
├── types.h # Common type definitions
├── macros.h # Utility macros
├── memory/ # Memory management APIs
│ ├── memory_api.h # Memory allocator API
│ ├── pointer_api.h # Pointer and type-safe memory access
│ └── memory_block.h # Block allocator implementation
├── ds/ # Data structures (queues, stacks, buffers, maps)
├── crc/ # CRC algorithms
├── time/ # Time management API
├── log/ # Logging API
├── sm/ # State machine API
├── sch/ # Scheduler API
├── sb/ # Service bus/broker API
├── io/ # I/O abstractions (SPI, I2C, UART, etc.)
├── hash/ # Hashing functions
└── math/ # Mathematical utilities
Example: Using the Module System
Define a Module
typedef struct MY_API_T MY_API_T;
typedef struct MY_ROOT_T MY_ROOT_T;
typedef struct MY_IMPL_T MY_IMPL_T;
typedef struct MY_API_T {
} MY_API_T;
int iCommonField;
) MY_ROOT_T;
int iPrivateState;
) MY_IMPL_T;
MY_IMPL_T tImpl;
);
#define JUNO_MODULE(API_T, ROOT_T,...)
Define a module union consisting of the root and all derivations.
Definition module.h:110
#define JUNO_MODULE_DERIVE(ROOT_T,...)
Implement a derived module embedding the root as the first member.
Definition module.h:157
#define JUNO_MODULE_ROOT(API_T,...)
Implement a module root struct containing ptApi and failure fields.
Definition module.h:126
Initialize and Use
MY_IMPL_T *ptImpl = (MY_IMPL_T *)ptModule;
ptImpl->iPrivateState++;
ptImpl->ptLog->ptApi->Info(ptImpl->ptLog, "Work done: %d", ptImpl->iPrivateState);
}
static const MY_API_T gMyApi = {
.DoWork = MyImpl_DoWork
};
ptImpl->JUNO_MODULE_SUPER.ptApi = &gMyApi;
ptImpl->JUNO_MODULE_SUPER.JUNO_FAILURE_HANDLER = NULL;
ptImpl->JUNO_MODULE_SUPER.JUNO_FAILURE_USER_DATA = NULL;
ptImpl->iPrivateState = 0;
ptImpl->ptLog = ptLog;
}
void UseModule(MY_ROOT_T *ptModule) {
ptModule->ptApi->DoWork(ptModule);
}
#define JUNO_STATUS_SUCCESS
Operation completed successfully.
Definition status.h:56
Traits vs Modules
LibJuno distinguishes between modules and traits:
- Modules (
JUNO_MODULE_ROOT): Have failure handlers and full module infrastructure
- Traits (
JUNO_TRAIT_ROOT): Lightweight, only carry the API pointer and data
Use traits for simple value types (like JUNO_POINTER_T) that don't need failure handling.
Memory Safety
LibJuno's approach to memory safety:
- No dynamic allocation in library code: All memory is provided by the caller
- Type-safe pointer API:
JUNO_POINTER_T carries size, alignment, and API information
- Explicit memory management: Block allocators with static pools
- Reference semantics: Clear ownership through API contracts
See memory/README.md for detailed memory management documentation.
Status and Error Handling
All fallible operations return JUNO_STATUS_T (or a JUNO_RESULT_T variant):
JUNO_STATUS_T tStatus = ptModule->ptApi->Operation(ptModule, ...);
}
Modules can optionally register failure handlers that are called on error conditions for logging/diagnostics without affecting control flow.
Freestanding Compatibility
LibJuno is designed to support freestanding builds (no hosted C standard library):
- Module roots avoid hosted types
- Build with
-DJUNO_FREESTANDING=ON to enable freestanding mode
- Adds
-ffreestanding -nostdlib flags
- Users provide their own printf/time implementations when needed
Note: Some examples and tests use hosted features for convenience, but the core library supports true freestanding operation.
Documentation and Examples
Design Philosophy
LibJuno prioritizes:
- Memory Safety: Type-safe APIs, no dynamic allocation, explicit bounds
- Software Scalability: Modular design, clear dependencies, easy to extend
- Shareability: Small, self-contained components easy to reuse across projects
- Transparency: All dependencies explicit, no hidden global state
- Portability: Freestanding support, minimal assumptions about target platform
The library does not prescribe:
- A specific RTOS or bare-metal environment
- A centralized executive or main loop
- Specific hardware abstractions
Instead, LibJuno provides tools that developers can compose to fit their specific requirements.
Getting Started
- Read the main README for build instructions
- Review the LibJuno Tutorial
- Explore minimal examples in
examples/
- Consult inline Doxygen documentation in headers
- Use template generators in
scripts/ to bootstrap your own modules
For questions or contributions, see the GitHub repository.