structecs

Crates.iostructecs
lib.rsstructecs
version0.4.0
created_at2025-11-02 15:20:11.11557+00
updated_at2025-11-11 04:11:08.455403+00
descriptionA structural data access framework. Type-safe extraction from nested structures with Arc-based smart pointers.
homepagehttps://github.com/moriyoshi-kasuga/structecs
repositoryhttps://github.com/moriyoshi-kasuga/structecs
max_upload_size
id1913219
size102,601
mori (moriyoshi-kasuga)

documentation

https://docs.rs/structecs

README

structecs

Crates.io Documentation License Rust

A structural data access framework for Rust.

Access your data structures with OOP-like ergonomics, manage them however you want.


⚠️ Development Status

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!


What is structecs?

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 ownership
  • Extractable - 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.

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

Why no World/Archetype/EntityID?

Early versions included ECS-style centralized management, but this was removed because:

  1. No global entity tracking needed - In a Minecraft server, entities are managed per-Chunk or per-World
  2. Arc wrapping makes centralized management redundant - Since Acquirable uses Arc internally, you can store and share references however you want
  3. User-defined organization is better - Different use cases need different organization:
    • Arc<RwLock<HashMap<UUID, Acquirable<Entity>>>> for entities
    • Arc<RwLock<HashMap<BlockPos, Acquirable<Block>>>> for blocks
    • Chunk-local collections, World-level collections, etc.

structecs provides the primitives (Acquirable, Extractable). You build the architecture.

Core Concepts

Acquirable<T> - Smart Pointers with Shared Ownership

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));

WeakAcquirable<T> - Weak References

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);
}

Extractable - Structural Type Extraction

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);

Design Philosophy

  • 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

Compile-time Safety

structecs provides two levels of type checking:

Runtime Checking (Default)

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());

Compile-time Checking (Checked APIs)

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? Because TypeId::eq() is not yet const-stable in Rust
  • Debug builds panic at compile time, release builds use unsafe for zero cost

Optional Archetype (Feature Flag)

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() requires U: 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.

Usage Examples

Basic Example

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.


Feature Flags

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

Resources


License

Licensed under MIT License.


Contributing

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.

Commit count: 0

cargo fmt