aissia/docs/GROVEENGINE_GUIDE.md
StillHammer 0dfb5f1535 chore: Normalize line endings and update project documentation
- Normalize CRLF to LF across all source files
- Replace CLAUDE.md.old with updated CLAUDE.md
- Standardize configuration file formatting
- Update module source files with consistent line endings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 22:13:16 +08:00

750 lines
22 KiB
Markdown

# GroveEngine User Guide
GroveEngine is a C++17 hot-reload module system designed for building modular applications with runtime code replacement capabilities.
## Table of Contents
1. [Overview](#overview)
2. [Core Concepts](#core-concepts)
3. [Project Setup](#project-setup)
4. [Creating Modules](#creating-modules)
5. [Module Lifecycle](#module-lifecycle)
6. [Inter-Module Communication](#inter-module-communication)
7. [Hot-Reload](#hot-reload)
8. [Configuration Management](#configuration-management)
9. [Task Scheduling](#task-scheduling)
10. [API Reference](#api-reference)
---
## Overview
GroveEngine provides:
- **Hot-Reload**: Replace module code at runtime without losing state
- **Modular Architecture**: Self-contained modules with clear interfaces
- **Pub/Sub Communication**: Decoupled inter-module messaging via topics
- **State Preservation**: Automatic state serialization across reloads
- **Configuration Hot-Reload**: Update module configuration without code changes
### Design Philosophy
- Modules contain pure business logic (200-300 lines recommended)
- No infrastructure code in modules (threading, networking, persistence)
- All data via `IDataNode` abstraction (backend agnostic)
- Pull-based message processing (modules control when they read messages)
---
## Core Concepts
### IModule
The base interface all modules implement. Defines the contract for:
- Processing logic (`process()`)
- Configuration (`setConfiguration()`, `getConfiguration()`)
- State management (`getState()`, `setState()`)
- Lifecycle (`shutdown()`)
### IDataNode
Hierarchical data structure for configuration, state, and messages. Supports:
- Typed accessors (`getString()`, `getInt()`, `getDouble()`, `getBool()`)
- Tree navigation (`getChild()`, `getChildNames()`)
- Pattern matching (`getChildrenByNameMatch()`)
### IIO
Pub/Sub communication interface:
- `publish()`: Send messages to topics
- `subscribe()`: Listen to topic patterns
- `pullMessage()`: Consume received messages
### ModuleLoader
Handles dynamic loading of `.so` files:
- `load()`: Load a module from shared library
- `reload()`: Hot-reload with state preservation
- `unload()`: Clean unload
---
## Project Setup
### Directory Structure
```
MyProject/
├── CMakeLists.txt
├── external/
│ └── GroveEngine/ # GroveEngine (submodule or symlink)
├── src/
│ ├── main.cpp
│ └── modules/
│ ├── MyModule.h
│ └── MyModule.cpp
└── config/
└── mymodule.json
```
### CMakeLists.txt
```cmake
cmake_minimum_required(VERSION 3.20)
project(MyProject VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# ============================================================================
# GroveEngine Integration
# ============================================================================
set(GROVE_BUILD_TESTS OFF CACHE BOOL "Disable GroveEngine tests" FORCE)
add_subdirectory(external/GroveEngine)
# ============================================================================
# Main Executable
# ============================================================================
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE
GroveEngine::impl
spdlog::spdlog
)
# ============================================================================
# Hot-Reloadable Modules (.so)
# ============================================================================
add_library(MyModule SHARED
src/modules/MyModule.cpp
)
target_link_libraries(MyModule PRIVATE
GroveEngine::impl
spdlog::spdlog
)
set_target_properties(MyModule PROPERTIES
PREFIX "lib"
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules
)
# ============================================================================
# Copy config files to build directory
# ============================================================================
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/config/
DESTINATION ${CMAKE_BINARY_DIR}/config)
# ============================================================================
# Convenience targets
# ============================================================================
add_custom_target(modules
DEPENDS MyModule
COMMENT "Building hot-reloadable modules only"
)
```
### Linking GroveEngine
Option 1: Git submodule
```bash
git submodule add <grove-engine-repo> external/GroveEngine
```
Option 2: Symlink (local development)
```bash
ln -s /path/to/GroveEngine external/GroveEngine
```
---
## Creating Modules
### Module Header
```cpp
// src/modules/MyModule.h
#pragma once
#include <grove/IModule.h>
#include <grove/IDataNode.h>
#include <grove/IIO.h>
#include <spdlog/spdlog.h>
#include <memory>
namespace myapp {
class MyModule : public grove::IModule {
public:
MyModule();
// Required IModule interface
void process(const grove::IDataNode& input) override;
void setConfiguration(const grove::IDataNode& configNode,
grove::IIO* io,
grove::ITaskScheduler* scheduler) override;
const grove::IDataNode& getConfiguration() override;
std::unique_ptr<grove::IDataNode> getHealthStatus() override;
void shutdown() override;
std::unique_ptr<grove::IDataNode> getState() override;
void setState(const grove::IDataNode& state) override;
std::string getType() const override { return "mymodule"; }
bool isIdle() const override { return true; }
private:
std::shared_ptr<spdlog::logger> m_logger;
std::unique_ptr<grove::IDataNode> m_config;
grove::IIO* m_io = nullptr;
// Module state (will be preserved across hot-reloads)
int m_counter = 0;
std::string m_status;
};
} // namespace myapp
// Required C exports for dynamic loading
extern "C" {
grove::IModule* createModule();
void destroyModule(grove::IModule* module);
}
```
### Module Implementation
```cpp
// src/modules/MyModule.cpp
#include "MyModule.h"
#include <grove/JsonDataNode.h>
#include <spdlog/sinks/stdout_color_sinks.h>
namespace myapp {
MyModule::MyModule() {
m_logger = spdlog::get("MyModule");
if (!m_logger) {
m_logger = spdlog::stdout_color_mt("MyModule");
}
m_config = std::make_unique<grove::JsonDataNode>("config");
}
void MyModule::setConfiguration(const grove::IDataNode& configNode,
grove::IIO* io,
grove::ITaskScheduler* scheduler) {
m_io = io;
m_config = std::make_unique<grove::JsonDataNode>("config");
// Read configuration values with defaults
m_status = configNode.getString("initialStatus", "ready");
int startCount = configNode.getInt("startCount", 0);
m_logger->info("MyModule configured: status={}, startCount={}",
m_status, startCount);
}
const grove::IDataNode& MyModule::getConfiguration() {
return *m_config;
}
void MyModule::process(const grove::IDataNode& input) {
// Get frame timing from input
double deltaTime = input.getDouble("deltaTime", 0.016);
int frameCount = input.getInt("frameCount", 0);
// Your processing logic here
m_counter++;
// Process incoming messages
while (m_io && m_io->hasMessages() > 0) {
auto msg = m_io->pullMessage();
m_logger->debug("Received message on topic: {}", msg.topic);
// Handle message...
}
// Publish events if needed
if (m_counter % 100 == 0) {
auto event = std::make_unique<grove::JsonDataNode>("event");
event->setInt("counter", m_counter);
m_io->publish("mymodule:milestone", std::move(event));
}
}
std::unique_ptr<grove::IDataNode> MyModule::getHealthStatus() {
auto status = std::make_unique<grove::JsonDataNode>("health");
status->setString("status", "running");
status->setInt("counter", m_counter);
return status;
}
void MyModule::shutdown() {
m_logger->info("MyModule shutting down, counter={}", m_counter);
}
// ============================================================================
// State Serialization (Critical for Hot-Reload)
// ============================================================================
std::unique_ptr<grove::IDataNode> MyModule::getState() {
auto state = std::make_unique<grove::JsonDataNode>("state");
// Serialize all state that must survive hot-reload
state->setInt("counter", m_counter);
state->setString("status", m_status);
m_logger->debug("State saved: counter={}", m_counter);
return state;
}
void MyModule::setState(const grove::IDataNode& state) {
// Restore state after hot-reload
m_counter = state.getInt("counter", 0);
m_status = state.getString("status", "ready");
m_logger->info("State restored: counter={}", m_counter);
}
} // namespace myapp
// ============================================================================
// C Export Functions (Required for dlopen)
// ============================================================================
extern "C" {
grove::IModule* createModule() {
return new myapp::MyModule();
}
void destroyModule(grove::IModule* module) {
delete module;
}
}
```
---
## Module Lifecycle
```
┌─────────────────────────────────────────────────────────────────┐
│ Module Lifecycle │
└─────────────────────────────────────────────────────────────────┘
1. LOAD
ModuleLoader::load(path, name)
dlopen() → createModule()
setConfiguration(config, io, scheduler)
Module Ready
2. PROCESS (Main Loop)
┌──────────────────────┐
│ process(input) │◄────┐
│ - Read deltaTime │ │
│ - Update state │ │
│ - Pull messages │ │
│ - Publish events │ │
└──────────────────────┘ │
│ │
└────────────────────┘
3. HOT-RELOAD
File change detected
getState() → Save state
unload() → dlclose()
load(path, name, isReload=true)
setConfiguration(config, io, scheduler)
setState(savedState)
Module continues with preserved state
4. SHUTDOWN
shutdown()
destroyModule()
dlclose()
```
---
## Inter-Module Communication
### IIO Pub/Sub System
Modules communicate via topics using publish/subscribe pattern.
#### Publishing Messages
```cpp
void MyModule::process(const grove::IDataNode& input) {
// Create message data
auto data = std::make_unique<grove::JsonDataNode>("data");
data->setString("event", "player_moved");
data->setDouble("x", 100.5);
data->setDouble("y", 200.3);
// Publish to topic
m_io->publish("game:player:position", std::move(data));
}
```
#### Subscribing to Topics
```cpp
void MyModule::setConfiguration(const grove::IDataNode& configNode,
grove::IIO* io,
grove::ITaskScheduler* scheduler) {
m_io = io;
// Subscribe to specific topic
m_io->subscribe("game:player:*");
// Subscribe with low-frequency batching (for non-critical updates)
grove::SubscriptionConfig config;
config.batchInterval = 1000; // 1 second batches
m_io->subscribeLowFreq("analytics:*", config);
}
```
#### Processing Messages
```cpp
void MyModule::process(const grove::IDataNode& input) {
// Pull-based: module controls when to process messages
while (m_io->hasMessages() > 0) {
grove::Message msg = m_io->pullMessage();
if (msg.topic == "game:player:position") {
double x = msg.data->getDouble("x", 0.0);
double y = msg.data->getDouble("y", 0.0);
// Handle position update...
}
}
}
```
### Topic Patterns
Topics support wildcard matching:
| Pattern | Matches |
|---------|---------|
| `game:player:*` | `game:player:position`, `game:player:health` |
| `economy:*` | `economy:prices`, `economy:trade` |
| `*:error` | `network:error`, `database:error` |
---
## Hot-Reload
### How It Works
1. **File Watcher** detects `.so` modification
2. **State Extraction**: `getState()` serializes module state
3. **Unload**: Old library closed with `dlclose()`
4. **Load**: New library loaded with `dlopen()` (cache bypass via temp copy)
5. **Configure**: `setConfiguration()` called with same config
6. **Restore**: `setState()` restores serialized state
### Implementing Hot-Reload Support
```cpp
// Critical: Serialize ALL state that must survive reload
std::unique_ptr<grove::IDataNode> MyModule::getState() {
auto state = std::make_unique<grove::JsonDataNode>("state");
// Primitives
state->setInt("counter", m_counter);
state->setDouble("health", m_health);
state->setString("name", m_name);
state->setBool("active", m_active);
// Complex state: serialize to JSON child nodes
auto entitiesNode = std::make_unique<grove::JsonDataNode>("entities");
for (const auto& entity : m_entities) {
auto entityNode = std::make_unique<grove::JsonDataNode>(entity.id);
entityNode->setDouble("x", entity.x);
entityNode->setDouble("y", entity.y);
entitiesNode->setChild(entity.id, std::move(entityNode));
}
state->setChild("entities", std::move(entitiesNode));
return state;
}
void MyModule::setState(const grove::IDataNode& state) {
// Restore primitives
m_counter = state.getInt("counter", 0);
m_health = state.getDouble("health", 100.0);
m_name = state.getString("name", "default");
m_active = state.getBool("active", true);
// Restore complex state
m_entities.clear();
auto* entitiesNode = state.getChildReadOnly("entities");
if (entitiesNode) {
for (const auto& name : entitiesNode->getChildNames()) {
auto* entityNode = entitiesNode->getChildReadOnly(name);
if (entityNode) {
Entity e;
e.id = name;
e.x = entityNode->getDouble("x", 0.0);
e.y = entityNode->getDouble("y", 0.0);
m_entities.push_back(e);
}
}
}
}
```
### File Watcher Example
```cpp
class FileWatcher {
public:
void watch(const std::string& path) {
if (fs::exists(path)) {
m_lastModified[path] = fs::last_write_time(path);
}
}
bool hasChanged(const std::string& path) {
if (!fs::exists(path)) return false;
auto currentTime = fs::last_write_time(path);
auto it = m_lastModified.find(path);
if (it == m_lastModified.end()) {
m_lastModified[path] = currentTime;
return false;
}
if (currentTime != it->second) {
it->second = currentTime;
return true;
}
return false;
}
private:
std::unordered_map<std::string, fs::file_time_type> m_lastModified;
};
```
---
## Configuration Management
### JSON Configuration Files
```json
// config/mymodule.json
{
"initialStatus": "ready",
"startCount": 0,
"maxItems": 100,
"debugMode": false,
"updateRate": 0.016
}
```
### Loading Configuration
```cpp
std::unique_ptr<grove::JsonDataNode> loadConfig(const std::string& path) {
if (fs::exists(path)) {
std::ifstream file(path);
nlohmann::json j;
file >> j;
return std::make_unique<grove::JsonDataNode>("config", j);
}
// Return empty config with defaults
return std::make_unique<grove::JsonDataNode>("config");
}
```
### Runtime Config Updates
Modules can optionally support runtime configuration changes:
```cpp
bool MyModule::updateConfig(const grove::IDataNode& newConfig) {
// Validate new config
int maxItems = newConfig.getInt("maxItems", 100);
if (maxItems < 1 || maxItems > 10000) {
m_logger->warn("Invalid maxItems: {}", maxItems);
return false;
}
// Apply changes
m_maxItems = maxItems;
m_debugMode = newConfig.getBool("debugMode", false);
m_logger->info("Config updated: maxItems={}", m_maxItems);
return true;
}
```
---
## Task Scheduling
For computationally expensive operations, delegate to `ITaskScheduler`:
```cpp
void MyModule::setConfiguration(const grove::IDataNode& configNode,
grove::IIO* io,
grove::ITaskScheduler* scheduler) {
m_scheduler = scheduler; // Store reference
}
void MyModule::process(const grove::IDataNode& input) {
// Delegate expensive pathfinding calculation
if (needsPathfinding) {
auto taskData = std::make_unique<grove::JsonDataNode>("task");
taskData->setDouble("startX", unit.x);
taskData->setDouble("startY", unit.y);
taskData->setDouble("targetX", target.x);
taskData->setDouble("targetY", target.y);
taskData->setString("unitId", unit.id);
m_scheduler->scheduleTask("pathfinding", std::move(taskData));
}
// Check for completed tasks
while (m_scheduler->hasCompletedTasks() > 0) {
auto result = m_scheduler->getCompletedTask();
std::string unitId = result->getString("unitId", "");
// Apply pathfinding result...
}
}
```
---
## API Reference
### IDataNode
| Method | Description |
|--------|-------------|
| `getString(name, default)` | Get string property |
| `getInt(name, default)` | Get integer property |
| `getDouble(name, default)` | Get double property |
| `getBool(name, default)` | Get boolean property |
| `setString(name, value)` | Set string property |
| `setInt(name, value)` | Set integer property |
| `setDouble(name, value)` | Set double property |
| `setBool(name, value)` | Set boolean property |
| `hasProperty(name)` | Check if property exists |
| `getChild(name)` | Get child node (transfers ownership) |
| `getChildReadOnly(name)` | Get child node (no ownership transfer) |
| `setChild(name, node)` | Add/replace child node |
| `getChildNames()` | Get names of all children |
### IIO
| Method | Description |
|--------|-------------|
| `publish(topic, data)` | Publish message to topic |
| `subscribe(pattern, config)` | Subscribe to topic pattern |
| `subscribeLowFreq(pattern, config)` | Subscribe with batching |
| `hasMessages()` | Count of pending messages |
| `pullMessage()` | Consume one message |
| `getHealth()` | Get IO health metrics |
### IModule
| Method | Description |
|--------|-------------|
| `process(input)` | Main processing method |
| `setConfiguration(config, io, scheduler)` | Initialize module |
| `getConfiguration()` | Get current config |
| `getState()` | Serialize state for hot-reload |
| `setState(state)` | Restore state after hot-reload |
| `getHealthStatus()` | Get module health report |
| `shutdown()` | Clean shutdown |
| `isIdle()` | Check if safe to hot-reload |
| `getType()` | Get module type identifier |
### ModuleLoader
| Method | Description |
|--------|-------------|
| `load(path, name, isReload)` | Load module from .so |
| `reload(module)` | Hot-reload with state preservation |
| `unload()` | Unload current module |
| `isLoaded()` | Check if module is loaded |
| `getLoadedPath()` | Get path of loaded module |
### IOFactory
| Method | Description |
|--------|-------------|
| `create(type, instanceId)` | Create IO by type string |
| `create(IOType, instanceId)` | Create IO by enum |
| `createFromConfig(config, instanceId)` | Create IO from config |
**IO Types:**
- `"intra"` / `IOType::INTRA` - Same-process (development)
- `"local"` / `IOType::LOCAL` - Same-machine (production single-server)
- `"network"` / `IOType::NETWORK` - Distributed (MMO scale)
---
## Building and Running
```bash
# Configure
cmake -B build
# Build everything
cmake --build build -j4
# Build modules only (for hot-reload workflow)
cmake --build build --target modules
# Run
./build/myapp
```
### Hot-Reload Workflow
1. Start application: `./build/myapp`
2. Edit module source: `src/modules/MyModule.cpp`
3. Rebuild module: `cmake --build build --target modules`
4. Application detects change and hot-reloads automatically
---
## Best Practices
1. **Keep modules small**: 200-300 lines of pure business logic
2. **No infrastructure code**: Let GroveEngine handle threading, persistence
3. **Serialize all state**: Everything in `getState()` survives hot-reload
4. **Use typed accessors**: `getInt()`, `getString()` with sensible defaults
5. **Pull-based messaging**: Process messages in `process()`, not callbacks
6. **Validate config**: Check configuration values in `setConfiguration()`
7. **Log appropriately**: Debug for development, Info for production events