toBridgedInstance method
Converts a native object to a bridged instance if a bridge exists.
nativeObject The native object to convert.
Returns a BridgedInstance if a bridge is found for the object's type, otherwise throws a D4rtException (caller should handle).
Resolution order:
- Direct lookup by Type (most specific — bridges with explicit BridgedClass.nativeType).
- BridgedClass.isAssignable iteration — keeps the LAST match across
all enclosing environments. Bridges register general→specific, so
the last match is the most specific (e.g., CupertinoTextThemeData
wins over Diagnosticable). Required to disambiguate native types
whose name happens to be a prefix of another bridge's name —
e.g.,
StringCharacters implements Characters: without this step the name-prefix fallback in toBridgedClass would wrap it asString. Bucket #11 fix. - toBridgedClass name-based fallbacks (private impl types,
generic suffix matching,
*Implprefix matching). Used only when neither direct type lookup nor isAssignable found a bridge.
Implementation
BridgedInstance? toBridgedInstance(Object? nativeObject) {
if (nativeObject == null) {
return null;
}
final runtimeType = nativeObject.runtimeType;
// 1) Direct type lookup.
Environment? current = this;
while (current != null) {
final direct = current._bridgedClassesLookupByType[runtimeType];
if (direct != null) {
return BridgedInstance(direct, nativeObject);
}
current = current._enclosing;
}
// 1b) Resolution cache. GEN-115 Phase 2: a previous call already paid
// the full step-2 / step-3 cost for this runtimeType; reuse it.
// Walks the env chain so a hit in any enclosing scope short-circuits.
current = this;
while (current != null) {
final cached = current._resolvedTypeCache[runtimeType];
if (cached != null) {
return BridgedInstance(cached, nativeObject);
}
// T4 (perf, F3): negative-cache hit — a previous call already proved this
// runtimeType has no bridge. Re-throw the same miss without re-walking the
// chain, iterating every bridge, or running the name-fallback toString.
if (current._unbridgedTypeCache.contains(runtimeType)) {
throw RuntimeD4rtException(
'Cannot bridge native object: No registered bridged class found '
'for native type $runtimeType.',
);
}
current = current._enclosing;
}
// 2) isAssignable iteration. Bridges may register in any order, so we
// collect ALL matches and then drop those that are supertypes of
// another match using [BridgedClass.transitiveSupertypeNames]. The
// remaining set is "leaf" matches; we pick the last one (preserves
// legacy LAST-wins behaviour when the registry doesn't disambiguate).
//
// D2 fix: A native object whose runtimeType is a private impl of
// BoxConstraints (e.g. `_BodyBoxConstraints`) was wrapped as
// `Constraints` because the LAST-match-wins iteration picked the
// abstract base. With `BoxConstraints: [Constraints, ...]` registered
// in the supertype registry, the filter drops `Constraints` and
// keeps `BoxConstraints`, so `.maxWidth` resolves correctly.
final allMatches = <BridgedClass>[];
current = this;
while (current != null) {
for (final entry in current._bridgedClassesLookupByType.entries) {
final bridge = entry.value;
if (bridge.isAssignable != null && bridge.isAssignable!(nativeObject)) {
allMatches.add(bridge);
}
}
current = current._enclosing;
}
if (allMatches.isNotEmpty) {
final filtered = _filterToMostSpecific(allMatches);
final picked = filtered.isNotEmpty ? filtered.last : allMatches.last;
_resolvedTypeCacheOrNew[runtimeType] = picked;
return BridgedInstance(picked, nativeObject);
}
// 3) Name-based fallbacks (private impl, generic suffix, *Impl prefix).
// [toBridgedClass] will throw if no bridge matches — try the structural
// fallback (step 4) before giving up, and negative-cache the miss so
// repeats short-circuit (T4, F3).
final BridgedClass bridgedClass;
try {
bridgedClass = toBridgedClass(runtimeType);
} on RuntimeD4rtException {
// 4) Structural suffix fallback (SCC49). Dart names implementation types
// after the interface they implement — `_CompactIterator`,
// `_SplayTreeKeyIterator`, `_HashMapKeyIterator`, `RuneIterator`,
// `_CompactKeysIterable` — and step 3 already exploits that, but only
// for names that are BOTH public AND generic: the suffix rule sits in
// the `else if (name.contains('<'))` arm of an
// `if (name starts with '_') … else if …` chain, so it is unreachable
// for private names (they take the first arm) and for non-generic
// public names (they enter neither). Those two shapes are the entire
// reason the stdlib bridges carry hand-maintained `nativeNames`
// allowlists: the `Iterator` bridge alone enumerates seventeen private
// SDK types, and the eighteenth an SDK release introduces surfaces to
// the script author as "Undefined property or method 'moveNext' on
// _CompactIterator".
//
// WHY IT LIVES HERE AND NOT IN `toBridgedClass`. It was implemented
// there first, as a fourth pass past the loose prefix fallback, on the
// reasoning that a pass firing only where an exception is already
// thrown cannot regress a working case. That reasoning is wrong, and
// the suite said so: 43 tests failed, all of them enum dispatch. The
// throw is not merely a failure report — callers USE it as a control
// flow signal, catching it and falling through to the bridged-ENUM
// registry, which is a different registry from bridged classes. A
// bridged enum named `SimpleEnum` suffix-matches the `Enum` bridge, so
// widening `toBridgedClass` silently claimed it and the fallthrough
// never ran. Resolving here instead keeps `toBridgedClass` — which
// `getRuntimeType` and others also call — exactly as it was, and
// confines the widening to the one path that actually wraps a native
// object.
//
// INTERPRETER-OWNED VALUES ARE EXCLUDED for the same reason, and the
// exclusion is wider than it first looks. The obvious guard —
// "skip native Dart enum values" — was not enough: the object that
// actually reached here in the enum tests was d4rt's own
// `BridgedEnum`, whose NAME ends with `Enum`, so it was claimed by the
// `Enum` bridge. Interpreter runtime objects (`RuntimeType`,
// `RuntimeValue`, `Callable`) are not native objects awaiting a
// bridge; they are the interpreter's own representation, and a
// name-shaped guess about them is never meaningful. Nothing about a
// suffix can distinguish them, so the guess must not be attempted.
//
// `nativeNames` stays the fast path and the explicit-ownership
// override: an entry there matches in step 3 and never reaches here,
// so a bridge can still claim a type whose name points elsewhere —
// which is required, because the SDK abbreviates often enough
// (`_StreamSinkWrapper` for `StreamSink`, `_ControllerSubscription`
// for `StreamSubscription`) that the naming convention alone is not
// sufficient.
final BridgedClass? structural = _isInterpreterOwned(nativeObject)
? null
: _structuralSuffixBridge(runtimeType);
if (structural != null) {
Logger.debug(
"[Environment] Matched native type '$runtimeType' to bridge "
"'${structural.name}' via structural suffix matching",
);
_resolvedTypeCacheOrNew[runtimeType] = structural;
return BridgedInstance(structural, nativeObject);
}
_unbridgedTypeCacheOrNew.add(runtimeType);
rethrow;
}
_resolvedTypeCacheOrNew[runtimeType] = bridgedClass;
return BridgedInstance(bridgedClass, nativeObject);
}