| Crates.io | structecs |
| lib.rs | structecs |
| version | 0.4.0 |
| created_at | 2025-11-02 15:20:11.11557+00 |
| updated_at | 2025-11-11 04:11:08.455403+00 |
| description | A structural data access framework. Type-safe extraction from nested structures with Arc-based smart pointers. |
| homepage | https://github.com/moriyoshi-kasuga/structecs |
| repository | https://github.com/moriyoshi-kasuga/structecs |
| max_upload_size | |
| id | 1913219 |
| size | 102,601 |
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:
World, Archetype, EntityID (centralized ECS management)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 typesYou 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:
Why no World/Archetype/EntityID?
Early versions included ECS-style centralized management, but this was removed because:
Acquirable uses Arc internally, you can store and share references however you wantArc<RwLock<HashMap<UUID, Acquirable<Entity>>>> for entitiesArc<RwLock<HashMap<BlockPos, Acquirable<Block>>>> for blocksstructecs provides the primitives (Acquirable, Extractable). You build the architecture.
Acquirable<T> - Smart Pointers with Shared OwnershipAcquirable<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));
WeakAcquirable<T> - Weak ReferencesPrevent 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);
}
Extractable - Structural Type ExtractionThe 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);
Arc<RwLock<...>> as neededstructecs 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)module_path!() + type name)TypeId? Because TypeId::eq() is not yet const-stable in Rustunsafe for zero costFor 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:
Clone (cheap Arc clone) + Send + Syncinsert() requires U: contains Baseinner() for custom operations; methods added only when neededAcquirable<Base>, extract to specific typesEnable 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:
Arc<RwLock<HashMap>>When NOT to use archetype:
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.