Repository navigation
Conversation
A managed object begins with a pointer to the vtable of its class, so the vtable address doubles as an identity handle: an object is an instance of this exact class if and only if its first word equals it. That is the only handle available when the object cannot be reached by walking static fields. Games built around constructor-injection dependency injection often have no static roots at all -- every static being a constant, an enum member or a key -- which leaves UnityPointer inapplicable and scanning the heap for a class's instance the only way in. The lookup is not new code so much as code that was already here: the Mono arm of Runtime::static_table resolved the vtable and then walked on to the static table, so it now calls this and keeps only the part that is its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An object begins with a pointer to its class's vtable and a vtable begins with a pointer to its class, so an object found by scanning can be resolved back to a Class and read by field name. This is the only route to a generic instantiation. HashSet<string> and List<Foo> are each a distinct class with their own field offsets, and none of them appears in an Image's class cache under a name that can be looked up -- but every instance carries a pointer to its own. Documented with the trap that comes with it: Mono fills a class's field table in lazily, and for an inflated generic nothing necessarily has, so get_field_offset can return None for every field of a class resolved this way against an object that is plainly an instance of it. Measured against a live game, where the identical lookup succeeded in a second process running the same build, so it is runtime state and no version check can predict it. Callers that must not fail on a collection need a fallback to the known layout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Janzert
added a commit
to Janzert/timberborn_autosplitter
that referenced
this pull request
Oct 17, 2026
#157 was written against the Mono code before upstream moved it into `managed/`, and no longer applied. It is closed, and #200 replaces it from the `mono-class-handles` branch, with a test per accessor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two accessors on
mono::Class, for games where the object of interest cannot be reached by walking static fields. Games built around dependency injection often have no static roots at all, which leavesUnityPointerinapplicable. In Timberborn, the game this came from, every static field in every game assembly is a constant, an enum member or a key. The way in is to scan the heap for the instance of a class, and these make that possible.This replaces #157, which was written against the Mono code before it moved into
managed/and no longer applies. Rebuilt on the new walk, both are now mostly exposure of what the crate already does internally.Class::get_vtableA managed object begins with a pointer to the vtable of its class, so the vtable address doubles as an identity handle: an object is an instance of this exact class if and only if its first word equals it. That is what a scan can match on.
The Mono arm of
Runtime::static_tablealready resolved the vtable before walking on to the static table. That prefix is nowMonoRuntime::vtable_address, whichstatic_tablecalls, so the diff is an extraction rather than new lookup code.Class::of_objectThe other direction: an object found by scanning resolves to a
Classthat can then be read by field name. It is a public face forWalk::object_class, which the scene manager already uses to name components.This is the only route to a generic instantiation.
HashSet<string>andList<Foo>are each a distinct class with their own field offsets, and none of them appears in anImage's class cache under a name that can be looked up. Every instance carries a pointer to its own though.One gotcha that cost me some debugging, so it's in the doc comment as well: Mono fills a class's field table in lazily, and for an inflated generic nothing necessarily has.
get_field_offsetcan returnNonefor every field of a class resolved this way, against an object that is plainly an instance of it. The identical lookup succeeded against a second process running the same build, so it is runtime state and no version check can predict it. Callers that must not fail on a collection need a fallback to the known layout.Tests
One test each in
mono/walk_tests.rs, on the existing hand-laid fixture:vtables_resolve_through_the_runtime_info:GameManager's vtable is the one its statics are found through, and a class with no runtime info has none.objects_resolve_to_their_class: the liveGameManagerinstance resolves to a class whosepointsfield reads back 777, and an object with a null vtable pointer resolves to nothing.The #157 versions of both have run against a shipping Unity 6000.3 game, on Windows and under Proton on Linux, in an auto splitter that depends on them:
get_vtableto locate services by heap scan, andof_objectto resolveList<T>andHashSet<string>fields by name, checked against known element counts. These versions have since run a full session of the same game, now on Unity 6000.5, under Proton.cargo test --all-features,cargo fmt --checkand the three wasm builds pass; clippy reports nothing new overmaster.🤖 Generated with Claude Code