Safe value construction, mutation & runtime dynamic casting#75
Open
NSExceptional wants to merge 17 commits into
Open
Safe value construction, mutation & runtime dynamic casting#75NSExceptional wants to merge 17 commits into
NSExceptional wants to merge 17 commits into
Conversation
A conformance's class will be nil when the conformance refers to a class that is only present in a newer SDK. For example, SDKAdImpression is only available on iOS 14.5. An app that uses SDKAdImpression would wrap it in `if @available` guards. While the conformance and class name is still present in the binary when it runs on iOS < 14.5, the class will be `nil`.
Make some Swift functions public so that the linker can see them when linking CEcho.
If they're not public, release builds would fail with undefined symbols errors:
Undefined symbols for architecture arm64:
"_lookupSection", referenced from:
__loadImageFunc in CEcho.o
"_registerProtocolConformances", referenced from:
__loadImageFunc in CEcho.o
"_registerProtocols", referenced from:
__loadImageFunc in CEcho.o
"_registerTypeMetadata", referenced from:
__loadImageFunc in CEcho.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
The .indirectTypeDescriptor case loaded the GOT slot as a non-optional UnsafeRawPointer, which traps when the slot holds null (e.g. a type that was weak-linked from a newer SDK and isn't present at runtime). Load it as an optional and return nil instead of crashing, matching how the .directObjCClass case already guards weak-linked classes.
Each __swift5_types entry is a RelativeDirectPointerIntPair<ContextDescriptor, TypeReferenceKind>, packing the reference kind into the low 2 bits of the relative offset. registerTypeMetadata treated every entry as a plain direct relative pointer, so indirect type-descriptor records (emitted for cross-module type references, e.g. when Foundation is imported) produced a misaligned pointer and crashed getContextDescriptor with "load from misaligned raw pointer". Mask off the kind bits and dereference the GOT slot for indirect records, matching ConformanceDescriptor's type-reference handling.
- classAddressPoint/classSize grew by one word in newer Swift's class metadata header; Echo reads them correctly (its _ClassMetadata layout matches TargetClassMetadata, and instanceSize/fieldOffsets still match), so update the stale constants (16->24, 120->128, 136->144). - Drop the NSObject classAddressPoint/instanceAddressPoint/alignmentMask assertions: reading Swift class-metadata fields off a pure Objective-C class inspects unrelated bytes (the old 32767 sentinel was meaningless). Keep the isSwiftClass == false invariant.
TypeContextDescriptorFlags.resilientSuperclassRefKind masked the 3-bit field at bit 9 (& 0xE00) but never shifted it down, so any non-direct kind produced a value like 0x200 that isn't a valid TypeReferenceKind raw value and trapped the force-unwrap. This crashes on any class with a resilient superclass referenced indirectly -- i.e. a cross-module resilient superclass such as a Foundation base class. Shift the masked field down by 9 (matching the sibling decode in RuntimeValues). Regression test reflects Boat3<String>: JSONEncoder, whose Foundation superclass is referenced indirectly; reading the kind trapped before.
The key-argument loop stored args[0].0 for every slot instead of args[i].0, so instantiating a generic type via the buffer path (4+ arguments, or 2-3 arguments with witness tables) wrote the first type argument into every position -- silent metadata corruption. Existing tests missed it because they instantiated with identical arguments (Double, Double), where storing args[0] repeatedly is accidentally correct. Regression test instantiates FooBaz2<Int, Double> with distinct, witness-table-bearing arguments, forcing the buffer path and asserting argument order is preserved.
This was referenced Jun 6, 2026
Introduces the write-side surface Jsum needs, as first-class Echo API: - StructMetadata.createInstance(fields:) builds a struct from a name-keyed dictionary of property values, via the value witnesses. - TypeMetadata.fieldRecords / fieldOffset(forKey:) / fieldType(forKey:) expose stored-property metadata by name (fieldType resolves the field's mangled type name through Echo's existing resolver). - AnyExistentialContainer.toAny / mutableValueBuffer() lift Jsum's box ergonomics into Echo (allocate a heap box on demand for out-of-line values; reinterpret a populated container as Any). Tests cover inline and out-of-line (boxed, reference-bearing) struct construction and field-metadata lookup. The read-back APIs (value(at:)/value(forKey:)) are intentionally deferred: building them surfaced a pre-existing bug where projectValue() mislocates out-of-line boxed values, now documented in GAPS.md as the next fix.
…ccess Root-caused the boxed-value failure: projectValue()'s offset math is correct; the hazard is a lifetime footgun where container(for:) boxes a non-inline value into its Any parameter and that box is freed on return, leaving the projected pointer dangling. (Verified by keeping the value alive: a 40-byte boxed struct projects correctly.) Adds: - withValuePointer(of:_:): lifetime-safe pointer access to an Any's value. - Metadata.allocateValueBuffer() / value(at:): caller-owned buffer round-trip. - AnyExistentialContainer.init(metadata:copying:). - TypeMetadata.value(forKey:from:) / value(forKey:of:): read a stored property by name (pointer-based and lifetime-safe Any-based). createInstance now reads field values through withValuePointer. Tests cover boxed round-trip and by-key reads. GAPS.md finding corrected.
- ClassMetadata.createInstance(fields:): allocate via swift_allocObject and initialize stored properties by name, walking Swift superclasses so inherited fields resolve. - TupleMetadata.createInstance(elements:): build a tuple positionally. - TypeMetadata/ClassMetadata.set(_:forKey:in:): assign over an existing stored property (destroys the old value, retains the new) by name. Tests cover inherited-field class construction, tuple building, and in-place struct/class mutation including reference-type fields.
The runtime equivalent of 'value as? T' with the target type supplied as Metadata (or Any.Type) rather than a static type. Copies the source into an owned buffer and casts with TakeOnSuccess|DestroyOnFailure so the runtime manages the value's ownership on both paths; returns nil on failure rather than trapping. Tests cover value success/failure, reference-type (String) casts, class up-casts, and unrelated-type failures.
65ab098 to
0c52760
Compare
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.
Important
Depends on #74 merging first
Summary
Adds a safe, supported surface for working with values whose type is only known at runtime — the capability the standard library's
Mirrorfundamentally lacks (it can only read). This is the foundation an object mapper / serializer needs: allocate an instance of a runtime-known type and populate it without callinginit, mutate stored properties by name, and read them back — plus a runtimedynamicCastfor when the target type isn't statically known.New Features
Value construction & access (
ValueConstruction.swift)StructMetadata/ClassMetadata/TupleMetadata.createInstance(...)— build an instance from name-keyed field values via the value witnesses (classes walk their Swift superclasses for inherited fields).TypeMetadata.set(_:forKey:in:),value(forKey:of:),fieldOffset(forKey:),fieldType(forKey:).withValuePointer(of:_:)— lifetime-safe access to anAny's storage (boxed values stay alive for the call), encapsulating a real footgun:container(for:)on an out-of-line value dangles once the temporaryAnyis released.Dynamic casting (
DynamicCast.swift)dynamicCast(_:to:)overswift_dynamicCast— the runtime equivalent ofas?with the target supplied asMetadata/Any.Type, for casts discovered through reflection. Returnsnilon failure rather than trapping.Tests
Tests cover inline and out-of-line (boxed, reference-bearing) struct/class/tuple construction, by-name mutation of value and reference types, boxed-value round-tripping, and value/class/failure dynamic casts.
swift testgreen (25 tests).