Skip to content

refactor: Make World !Send/!Sync and overhaul internal soundness - #299

Merged
Indra-db merged 34 commits into
mainfrom
fixes
Jul 17, 2026
Merged

Indra-db merged 34 commits into
mainfrom
fixes

Conversation

@Indra-db

Copy link
Copy Markdown
Owner

Changelog

⚠️ Breaking Changes

  • Thread Safety Changes: World is now !Send/!Sync (one world per thread). In exchange, components no longer need to be Send — you can now store Rc, raw pointers, etc. Query is Send/Sync only when its component types are.
  • Aliasing Protection: Duplicate mutable terms within a single tuple are now caught directly without relying on the safety-locks feature: get::<(&mut Pos, &Pos)>() now panics (same entity, always aliases).
  • Hook Behavior: on_replace hooks only fire when the component already existed (prev is always a valid value). First-time sets (including everything inside one deferred batch) no longer invoke the hook.
  • Unsafe Functions: Generated cast/cast_mut (query traits) and Table::new are now unsafe fn.
  • Lifetime Constraints: Iter::field/field_mut results can no longer outlive the iterator borrow.
  • API Visibility & Constraints: OS API hooks must be Send. World::components_array/components_map are no longer public.
  • Pointer & Send/Sync Changes: Removed Send/Sync from Opaque, EcsIdentifier, EcsPoly, and EntityView<'static>. Raw *mut ecs_world_t no longer converts to WorldRef in safe code — use unsafe WorldRef::from_ptr.

✨ Added

  • Fallible API Variants: Non-panicking variants: try_field (+ FieldError), try_build, try_assign*, try_first_id/try_second_id, and try_term_at_type.
  • Stages: App and AsyncStage owning handles, world.stage(), and merge().
  • Traits & Re-exports: Hash for EntityView/IdView; Debug/Eq/Display for systems, observers, and alerts; ClonedTuple re-export.
  • Macro Diagnostics: Derive/DSL macro errors now show as proper compiler diagnostics with spans.
  • Usability: set_first/assign_* now accept impl IntoEntity (more types work, existing code is unaffected).

🐛 Fixed

  • Panic Safety: Panics in component lifecycle hooks now abort instead of corrupting world state. Deferred operations and scopes are now panic-safe.
  • Soundness Pass: Fixed ~50 audited UB/crash bugs across script, query, observer, table, field, and registration internals. Hardened bounds and null checks throughout, backed by ~40 new test files.

Indra-db added 30 commits July 4, 2026 08:01
BREAKING CHANGE: ComponentId no longer requires Send + Sync;
World/WorldRef are !Send, Query is Send/Sync only when its tuple is,
par_* systems bound their tuples, thread-bound components panic on
off-thread access. App::run no longer steals a world refcount
(use-after-free); frame_action/run_action added, app runs serialized.
WorldRef: Deref<Target=World> transmutes &WorldRef -> &World, sound only
if both share layout. World was repr(Rust) while WorldRef was repr(C), so
field order was not guaranteed. Mark World repr(C) and add a const size/align
assertion at the transmute so any future layout drift fails to compile.
The IMPLS_SEND/IMPLS_SYNC probe (ImplementsSend::<T>::IMPLS) relies on the
blanket DoesNotImpl fallback for types that are not Send/Sync, but that trait
was not in scope in the component-trait macros. Any !Send/!Sync binding-type
component therefore failed to compile, which had been masked by giving Rest
bogus `unsafe impl Send/Sync` over its raw pointers (a live server handle).

Bring DoesNotImpl into scope inside each IMPLS_SEND/IMPLS_SYNC const block so
the probe resolves to false instead of erroring, and drop Rest's fake impls so
it is honestly !Send/!Sync and guarded by the world's thread-affinity checks.
cast_mut returned &'a mut dyn Trait from a safe fn taking a Copy EntityView,
with the lifetime tied to the world borrow rather than a unique borrow. Two
calls for the same entity+id minted aliasing &mut in safe code (UB). Mark it
unsafe and document the no-aliasing precondition. cast (shared) stays safe
since a &mut can no longer be created without unsafe.
The callback trampolines use extern "C-unwind" so a panic in a user
system/observer/hook unwinds back to the Rust caller (e.g. the app loop's
catch_unwind). That unwind passes through flecs C frames, which is only defined
if the C carries unwind info. flecs.c was compiled without it, making the
unwind UB on targets that omit unwind tables by default. Add -fexceptions and
-fasynchronous-unwind-tables on non-wasm (ignored where unsupported).
…ed entities/get_mut, range offset, gated multi_src_get, unlock on panic)
… dense field_at, borrow-tied field lifetimes, null guards, and public try_field/FieldError
…strings, run_each_iter consistency, Display impls, unwind tables)
…and table locks, docs for is_valid/has_enum/child
…gn and set_ptr checks, try_assign API, IntoEntity widening
…server null derefs, non-static term names, Debug impls, try_term_at_type)
…omponent array growth to index, abort on lifecycle hook panics
…read-local, drop double ecs_opaque_init, null-check enum constant ensure
…inter layout assert, fix ecs_get_ptr_t Default recursion
…ros, add try_first_id/try_second_id, document core/world modules, re-export assert_is_type
@Indra-db
Indra-db merged commit 33e2d93 into main Jul 17, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant