A structural data access framework for Rust.
Access your data structures with OOP-like ergonomics, manage them however you want.
This crate is currently under active development and undergoing major refactoring.
Version 0.3.x is a complete breaking change from earlier versions:
- Removed:
World,Archetype,EntityID(centralized ECS management) - Focus: Structural type access with user-managed collections
The API is not stable and may change significantly. Use at your own risk. Feedback and contributions are welcome!
structecs is a structural data access framework that lets you work with nested data structures using type-safe extraction and smart pointers.
Instead of managing entities in a central ECS World, structecs gives you the tools to build your own data management patterns:
Acquirable<T>- Arc-based smart pointers for shared ownershipExtractable- Derive macro for structural type extraction from nested types- Type-safe extraction - Access nested components through their parent structures
You manage your data structures (HashMap, BTree, custom collections) however you want. structecs just makes accessing nested types ergonomic and type-safe.
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
Why no World/Archetype/EntityID?
Early versions included ECS-style centralized management, but this was removed because:
- No global entity tracking needed - In a Minecraft server, entities are managed per-Chunk or per-World
- Arc wrapping makes centralized management redundant - Since
AcquirableusesArcinternally, you can store and share references however you want - User-defined organization is better - Different use cases need different organization:
Arc<RwLock<HashMap<UUID, Acquirable<Entity>>>>for entitiesArc<RwLock<HashMap<BlockPos, Acquirable<Block>>>>for blocks- Chunk-local collections, World-level collections, etc.
structecs provides the primitives (Acquirable, Extractable). You build the architecture.
Acquirable<T> is an Arc-based smart pointer that allows shared ownership of data with transparent access through Deref.
use structecs::*;
#[derive(Extractable)]
struct Player {
name: String,
health: u32,
}
let player = Acquirable::new(Player {
name: "Steve".to_string(),
health: 100,
});
// Access through Deref
println!("Player: {}, Health: {}", player.name, player.health);
// Clone creates a new reference to the same data
let player_ref = player.clone();
assert!(player.ptr_eq(&player_ref));Prevent circular references and implement cache-like structures with weak references:
use structecs::*;
#[derive(Extractable)]
struct Player {
name: String,
health: u32,
}
let player = Acquirable::new(Player { name: "Alex".to_string(), health: 80 });
let weak = player.downgrade();
// Upgrade when needed
if let Some(player_ref) = weak.upgrade() {
println!("Player still alive: {}", player_ref.name);
}The Extractable derive macro enables type-safe extraction of nested components:
use structecs::*;
#[derive(Extractable)]
struct Health {
current: u32,
max: u32,
}
#[derive(Extractable)]
#[extractable(health)] // Mark nested Extractable fields
struct LivingEntity {
id: u32,
health: Health,
}
#[derive(Extractable)]
#[extractable(living)]
struct Player {
name: String,
living: LivingEntity,
}
let player = Acquirable::new(Player {
name: "Steve".to_string(),
living: LivingEntity {
id: 42,
health: Health { current: 80, max: 100 },
},
});
// Extract nested types
let health: Acquirable<Health> = player.extract::<Health>().unwrap();
assert_eq!(health.current, 80);
let living: Acquirable<LivingEntity> = player.extract::<LivingEntity>().unwrap();
assert_eq!(living.id, 42);- No centralized storage - You manage your own collections and data structures
- OOP-like structural access - Access nested types through parent structures naturally
- User-controlled concurrency - Wrap your collections in
Arc<RwLock<...>>as needed - Type-safe extraction - The derive macro ensures compile-time safety for nested type access
- Minimal runtime overhead - Offset-based extraction with zero-cost abstractions
structecs provides two levels of type checking:
use structecs::*;
#[derive(Extractable)]
struct Entity { id: u32 }
#[derive(Extractable)]
#[extractable(entity)]
struct Player { name: String, entity: Entity }
let player = Acquirable::new(Player {
name: "Steve".to_string(),
entity: Entity { id: 1 },
});
// Returns Option - safe runtime check
let entity: Option<Acquirable<Entity>> = player.extract::<Entity>();
assert!(entity.is_some());For performance-critical paths, use _checked variants that validate at compile time:
use structecs::*;
#[derive(Extractable)]
struct Entity { id: u32 }
#[derive(Extractable)]
#[extractable(entity)]
struct Player { name: String, entity: Entity }
// Compile-time validation - panics at compile time if Player doesn't contain Entity
let player: Acquirable<Player> = Acquirable::new_checked(Player {
name: "Steve".to_string(),
entity: Entity { id: 1 },
});
// No runtime Option check needed - guaranteed to succeed
let entity: Acquirable<Entity> = player.extract_checked::<Entity>();How it works:
ExtractionMetadata::is_has<Container, Target>()runs at compile time (const evaluation)- Uses string-based type identification (
module_path!()+ type name) - Why not
TypeId? BecauseTypeId::eq()is not yet const-stable in Rust - Debug builds panic at compile time, release builds use
unsafefor zero cost
For common use cases, structecs provides an optional Archetype<Key, Base> collection:
use structecs::{Archetype, Extractable, Acquirable};
#[derive(Extractable)]
struct Entity { id: u32 }
#[derive(Extractable)]
#[extractable(entity)]
struct Player { name: String, entity: Entity }
// Compile-time checked: can only insert types containing Entity
let entities: Archetype<u32, Entity> = Archetype::default();
let player = Player {
name: "Alice".to_string(),
entity: Entity { id: 1 },
};
// Stores as Acquirable<Entity>, but accepts any U containing Entity
entities.insert(1, player);
// Retrieve as base type
let entity = entities.get(&1).unwrap();
// Extract back to specific type
let player_ref = entity.extract::<Player>().unwrap();
assert_eq!(player_ref.name, "Alice");Key Features:
- Thread-safe:
Clone(cheap Arc clone) +Send + Sync - Compile-time validated:
insert()requiresU: contains Base - Minimal API: Access
inner()for custom operations; methods added only when needed - Type flexibility: Stores as
Acquirable<Base>, extract to specific types
Enable with:
[dependencies]
structecs = { version = "0.3", features = ["archetype"] }Design Philosophy:
Archetype is intentionally minimal. For custom operations, use:
use structecs::*;
#[derive(Extractable)]
struct Entity { id: u32 }
let entities: Archetype<u32, Entity> = Archetype::default();
// Access the underlying Arc<RwLock<HashMap>> for custom operations
let map = entities.read(); // or .write() for mutations
// Custom iteration, filtering, etc.use structecs::*;
#[derive(Extractable)]
struct Entity {
id: u32,
}
#[derive(Extractable)]
#[extractable(entity)]
struct NamedEntity {
name: String,
entity: Entity,
}
#[derive(Extractable)]
#[extractable(entity)]
struct BlockEntity {
block_type: String,
entity: Entity,
}
let named = Acquirable::new(NamedEntity {
name: "Steve".to_string(),
entity: Entity { id: 42 },
});
let block = Acquirable::new(BlockEntity {
block_type: "Stone".to_string(),
entity: Entity { id: 43 },
});
let entities: Vec<Acquirable<Entity>> = vec![named.extract().unwrap(), block.extract().unwrap()];
for entity in entities {
println!("Entity ID: {}", entity.id);
}The key insight: You decide how to organize your data. Per-chunk HashMap? Per-world BTreeMap? Custom spatial index? It's all up to you.
structecs provides optional features that can be enabled in your Cargo.toml:
| Feature | Description | Default |
|---|---|---|
archetype |
Provides Archetype<Key, Base> - a thread-safe, type-checked HashMap wrapper for storing entities by a common base type. Useful for quick prototyping or simple use cases. |
❌ Disabled |
Example: Enabling features
[dependencies]
# Enable the archetype feature
structecs = { version = "0.3", features = ["archetype"] }
# Or use default (no features)
structecs = "0.3"When to use archetype:
- ✅ You want a pre-built collection for storing entities
- ✅ You need thread-safe access with
Arc<RwLock<HashMap>> - ✅ You're prototyping and don't want to build custom storage
When NOT to use archetype:
- ❌ You need custom storage structures (spatial indexes, quad-trees, etc.)
- ❌ You want full control over locking strategies
- ❌ You're building a specialized data management system
- API Documentation - Full API reference
- Examples - Practical usage patterns
- Crates.io - Published versions
Licensed under MIT License.
This project is in early development. Feedback, ideas, and contributions are welcome!
If you have suggestions or find issues, please open an issue or pull request on GitHub.