# structecs
[](https://crates.io/crates/structecs)
[](https://docs.rs/structecs)
[](https://github.com/moriyoshi-kasuga/structecs/blob/main/LICENSE)
[](https://www.rust-lang.org)
**A flexible entity-component framework without the System.**
Manage your data like ECS, control your logic like OOP.
---
## ⚠️ Development Status
This crate is currently under active development. The API is not stable and may change significantly.
Currently, performance is quite poor compared to established ECS frameworks.
Use at your own risk. Feedback and contributions are welcome!
---
## What is structecs?
`structecs` is an ECS-inspired data management framework designed for complex applications like game servers where traditional ECS systems can be limiting.
Unlike conventional ECS frameworks (Bevy, specs, hecs), structecs:
- ✅ **No rigid System architecture** - Write your logic however you want
- ✅ **Hierarchical components** - Nest components naturally like OOP
- ✅ **Dynamic type extraction** - Query for any component type on-the-fly
- ✅ **Zero-cost abstractions** - Uses compile-time offsets for component access
- ✅ **Thread-safe** - Concurrent operations via lock-free maps + short locks
## Quick Start
```rust
use structecs::*;
// Define your entities with the Extractable derive macro
#[derive(Debug, Extractable)]
pub struct Entity {
pub name: String,
}
#[derive(Debug, Extractable)]
#[extractable(entity)] // Mark nested extractable fields
pub struct Player {
pub entity: Entity,
pub health: u32,
}
fn main() {
// Create the world
let world = World::default();
// Add entities
let player_id = world.add_entity(Player {
entity: Entity {
name: "Hero".to_string(),
},
health: 100,
});
// Query all players
for (id, player) in world.query::<Player>() {
println!("[{}] {}: {} HP", id, player.entity.name, player.health);
}
// Extract nested components
for (id, entity) in world.query::<Entity>() {
println!("Entity: {}", entity.name);
// Try to extract as Player
if let Some(player) = entity.extract::<Player>() {
println!(" -> Player with {} HP", player.health);
}
}
// Batch remove entities efficiently (silent, no error tracking)
let ids_to_remove = vec![player_id /* ... */];
world.remove_entities(&ids_to_remove);
// Check entity existence
if world.contains_entity(&player_id) {
println!("Player still exists");
}
// Clear all entities
world.clear();
}
```
### When to use structecs?
**Good for:**
- Complex game servers (Minecraft, MMOs) with intricate entity relationships
- Applications where game logic doesn't fit cleanly into Systems
- Projects transitioning from OOP to data-oriented design
- Scenarios requiring flexible, ad-hoc data access patterns
**Not ideal for:**
- Simple games where traditional ECS works well
- Projects heavily invested in existing ECS ecosystems
- Use cases requiring absolute maximum performance
---
## Core Concepts
### 1. Entity
An `Entity` is just an ID - a lightweight handle to your data.
```rust
pub struct EntityId {
id: u32,
}
impl EntityId {
pub fn from_raw(id: u32) -> Self; // Create from raw u32
}
// EntityId implements Display: "Entity(123)"
println!("{}", entity_id);
```
Entities don't "own" components. Instead, they reference structured data stored in the `World`.
### 2. Component (via Extractable)
In structecs, components are **fields within structs**. The `Extractable` trait allows the framework to understand your data structure and extract specific types.
```rust
use structecs::*;
#[derive(Debug, Extractable)]
pub struct Entity {
pub name: String,
}
#[derive(Debug, Extractable)]
#[extractable(entity)]
pub struct Player {
pub entity: Entity,
pub health: u32,
}
```
**Key insight:** Components are hierarchical. A `Player` might contain an `Entity`, which itself is extractable.
### 3. World
The central data store that manages all entities and their data.
```rust
let world = World::default();
// Add entities
let player_id = world.add_entity(Player {
entity: Entity {
name: "Hero".to_string(),
},
health: 100,
});
// Query entities
for (id, player) in world.query::<Player>() {
println!("[{}] {}: {} HP", id, player.entity.name, player.health);
}
// Batch operations
let ids = vec![id1, id2, id3];
world.remove_entities(&ids); // Efficient batch removal (silent)
// Check existence
if world.contains_entity(&player_id) {
world.remove_entity(&player_id);
}
```
**Core operations:**
- `add_entity<E: Extractable>(entity: E) -> EntityId` - Register new entity
- `add_entity_with_acquirable<E: Extractable>(entity: E) -> (EntityId, Acquirable<E>)` - Add entity and get reference
- `add_entities<E: Extractable>(entities: impl IntoIterator<Item = E>) -> Vec<EntityId>` - Bulk add entities (optimized)
- `remove_entity(entity_id: &EntityId) -> Result<(), WorldError>` - Remove single entity from world
- `try_remove_entities(entity_ids: &[EntityId]) -> Result<(), WorldError>` - Batch remove entities with error tracking
- `remove_entities(entity_ids: &[EntityId])` - Fast batch removal (silently skips not found)
- `contains_entity(entity_id: &EntityId) -> bool` - Check if entity exists in world
- `clear()` - Remove all entities from world
- `query<T: 'static>() -> QueryIter<T>` - Lazy iterator over entities with component T
- `extract_component<T>(entity_id: &EntityId) -> Result<Acquirable<T>, WorldError>` - Get specific component
- `entity_count() -> usize` - Get total number of entities
- `archetype_count() -> usize` - Get number of archetypes
### 4. Acquirable
A smart reference to a component that keeps the underlying entity data alive.
```rust
pub struct Acquirable<T: 'static> {
target: NonNull<T>,
inner: EntityData,
}
```
**Features:**
- Implements `Deref<Target = T>` for transparent access
- Reference-counted to prevent use-after-free
- Can `extract()` other component types from the same entity
This enables OOP-like method chaining:
```rust
entity.extract::<Player>()?.extract::<Health>()?
```
### 5. Extractor
The engine that performs type extraction using pre-computed offsets.
```rust
pub struct Extractor {
offsets: FxHashMap<TypeId, usize>,
dropper: unsafe fn(NonNull<u8>),
}
```
Each unique entity structure gets one `Extractor` (cached in `World`), which knows:
- Where each component type lives in memory (offset)
- How to safely drop the entity when done
---
## Error Handling
structecs provides a comprehensive error type for handling failures:
```rust
pub enum WorldError {
/// The specified entity was not found in the world
EntityNotFound(EntityId),
/// The requested component type was not found on the entity
ComponentNotFound {
entity_id: EntityId,
component_name: &'static str,
},
/// Batch removal completed with some failures
/// Contains successfully removed and failed entity IDs
PartialRemoval {
succeeded: Vec<EntityId>,
failed: Vec<EntityId>,
},
/// Internal consistency error (should not occur in normal use)
ArchetypeNotFound(EntityId),
}
```
**Error handling examples:**
```rust
// Single entity removal with error handling
match world.remove_entity(&entity_id) {
Ok(()) => println!("Entity removed"),
Err(WorldError::EntityNotFound(id)) => println!("Entity {} not found", id),
Err(e) => println!("Error: {}", e),
}
// Batch removal with error tracking
match world.try_remove_entities(&entity_ids) {
Ok(()) => println!("All entities removed"),
Err(WorldError::PartialRemoval { succeeded, failed }) => {
println!("Removed: {}, Failed: {}", succeeded.len(), failed.len());
}
Err(e) => println!("Error: {}", e),
}
// Component extraction with error handling
match world.extract_component::<Health>(&entity_id) {
Ok(health) => println!("Health: {}", health.value),
Err(WorldError::EntityNotFound(id)) => println!("Entity not found"),
Err(WorldError::ComponentNotFound { entity_id, component_name }) => {
println!("Component {} not found on entity {}", component_name, entity_id);
}
Err(e) => println!("Error: {}", e),
}
```
---
## Architecture
For detailed architecture documentation, see [Architecture.md](Architecture.md).
### Memory Layout
**Archetype-based Storage:**
Entities with the same structure (type) are grouped into archetypes for better cache locality:
```
World:
Archetype<Player>:
[Entity 0] Player { entity: Entity { name: "A" }, health: 100 }
[Entity 1] Player { entity: Entity { name: "B" }, health: 80 }
[Entity 2] Player { entity: Entity { name: "C" }, health: 90 }
Archetype<Monster>:
[Entity 3] Monster { entity: Entity { name: "X" }, damage: 20 }
[Entity 4] Monster { entity: Entity { name: "Y" }, damage: 30 }
```
**Component Extraction:**
```
Player struct in memory:
┌─────────────────────────────┐
│ Entity { name: String } │ ← offset 0: Entity
│ ├─ name: String │ ← offset 0: String
├─────────────────────────────┤
│ health: u32 │ ← offset X: u32
└─────────────────────────────┘
The Extractor knows:
- TypeId(Entity) -> offset 0
- TypeId(String) -> offset 0 (flattened from Entity)
- TypeId(u32) -> offset X
```
### Data Flow
1. **Entity Registration:**
```
User creates struct → Derive macro generates METADATA_LIST
→ add_entity() creates Extractor → Data stored in World
```
2. **Component Query:**
```
query<T>() → Iterate all entities → Check if Extractor has TypeId(T)
→ Calculate pointer via offset → Wrap in Acquirable
```
3. **Component Extraction:**
```
Acquirable<A>.extract<B>() → Reuse same Extractor
→ Get offset for TypeId(B) → Return new Acquirable<B>
```
### Design Philosophy
**"Data is hierarchical, access is flat"**
- Store entities as natural Rust structs (hierarchical)
- Query any component type regardless of nesting (flat access)
- No forced System architecture (user controls logic flow)
This gives you:
- **Expressiveness** of OOP (nested data, clear relationships)
- **Flexibility** of procedural code (write systems however you want)
### Mutability Design
structecs provides **read-only access** by default. Users control mutability explicitly using Rust's interior mutability patterns:
```rust
use std::sync::{Mutex, RwLock};
use std::sync::atomic::AtomicU32;
// Pattern 1: Lock-free with Atomics
#[derive(Extractable)]
pub struct Player {
pub name: String,
pub health: AtomicU32, // Lock-free concurrent updates
}
// Pattern 2: Fine-grained locking with Mutex
#[derive(Extractable)]
pub struct Inventory {
pub items: Mutex<Vec<Item>>, // Lock only when accessing items
}
// Pattern 3: Read/write separation with RwLock
#[derive(Extractable)]
pub struct Position {
pub coords: RwLock<Vec3>, // Multiple readers, single writer
}
// Usage
for (id, player) in world.query::<Player>() {
player.health.fetch_add(10, Ordering::Relaxed);
}
```
**Why no `query_mut()`?**
- **Prevents lock contention**: Locking the entire World would block all operations
- **Fine-grained control**: Users choose the optimal locking strategy per component
- **Multi-threading first**: Essential for high-concurrency scenarios like game servers
This design philosophy prioritizes flexibility and performance in concurrent environments.
---
## Testing
structecs has a comprehensive test suite:
**Run tests:**
```bash
# Run all tests
cargo test --all
# Run specific test suite
cargo test --test integration_test
cargo test --test concurrent_test
cargo test --test memory_safety_test
cargo test --test edge_cases_test
```
---
## Comparison with Traditional ECS
| **Entity** | Opaque ID | Opaque ID ✓ |
| **Component** | Standalone data types | Fields in structs |
| **System** | First-class concept with scheduling | User implements freely |
| **Data Layout** | Archetype/sparse sets | Archetype-based ✓ |
| **Query Pattern** | Compile-time system parameters | Runtime extraction |
| **Nesting** | Components are flat | Components can nest ✓ |
| **Cache Coherency** | Excellent (packed arrays) | Good (archetype storage) |
| **Flexibility** | Constrained by System API | Maximum flexibility ✓ |
---
## Motivation
This framework was created for building a Minecraft server in Rust, where:
- Entity relationships are complex (Player ⊂ LivingEntity ⊂ Entity)
- Game logic is too varied to fit into rigid Systems
- OOP patterns are familiar but Rust's ownership makes traditional OOP difficult
structecs bridges the gap: data-oriented storage with OOP-like access patterns.
---
## License
Licensed under:
- MIT License
---
## Contributing
This project is in early development. Feedback, ideas, and contributions are welcome!