substratevm/docs/terminus-migration.md
This document captures the Project Terminus Migration Guide. It is intended for Native Image developers migrating build-time code away from host reflection and toward Terminus-safe guest-context APIs.
The primary API for working with the guest context is GuestAccess.
When migrating code for Terminus, prefer JVMCI- and VMAccess-based APIs over Java core reflection whenever the code interacts with guest-loaded types, methods, fields, modules, or invocation paths.
| Core reflection / hosted API | Terminus-safe replacement | Notes |
|---|---|---|
RuntimeReflection | JVMCIRuntimeReflection | Use Terminus-safe registration paths instead of host reflection state. |
RuntimeJNIAccess | JVMCIRuntimeJNIAccess | Prefer guest-aware access paths. |
RuntimeProxyCreation | JVMCIRuntimeProxyCreation | Avoid assuming host reflection ownership. |
JNIAccess | JVMCIJNIAccess | Same class name pattern with JVMCI prefix. |
AccessCondition | JVMCIAccessCondition | Use the JVMCI-aware variant. |
FeatureAccess.findClassByName() | InternalFeatureAccess.findTypeByName() | Use type lookup instead of host Class<?> lookup. |
Class.getDeclaredMethod() | JVMCIReflectionUtil.getUniqueDeclaredMethod(...) | Returns null when name and parameter types do not uniquely identify a method. |
Class.getDeclaredConstructor() | JVMCIReflectionUtil.getDeclaredConstructor(...) | Same uniqueness caveat as method lookup. |
Class.getConstructors() | JVMCIReflectionUtil.getConstructors(...) | Keep lookup in JVMCI space. |
Class.getDeclaredMethods() | ResolvedJavaType.getAllMethods(true) | The true value for forceLink is important so methods with bytecodes have non-null ResolvedJavaMethod.getCode(). |
Class.getDeclaredFields() | JVMCIReflectionUtil.getAllFields(ResolvedJavaType) | JVMCI includes internal fields (ResolvedJavaField.isInternal()), unlike core reflection. |
Class.getTypeName() | JVMCIReflectionUtil.getTypeName(ResolvedJavaType) | Direct type-name replacement. |
Class.getCanonicalName() | No direct replacement | Usually only needed for reporting; prefer suitable JavaType / ResolvedJavaType naming helpers. |
Class.getSimpleName() | JavaType.toJavaName() | Not a 1:1 semantic replacement; use mainly for reporting. |
Module, ModuleLayer, Package | ResolvedJavaModule, ResolvedJavaModuleLayer, ResolvedJavaPackage | These are the JVMCI-side equivalents used during Terminus migration. |
ModuleLayer.boot() | JVMCIReflectionUtil.bootModuleLayer() | Use the JVMCI boot layer view. |
ModuleLayer.boot().findModule(moduleName) | JVMCIReflectionUtil.bootModuleLayer().findModule(moduleName) | Common in afterRegistration; transitional and expected to become less important after migration. |
clazz.getProtectionDomain().getCodeSource().getLocation() | JVMCIReflectionUtil.getOrigin(type) | Use JVMCI type origin lookup. |
BootLoader.packages() | JVMCIReflectionUtil.bootLoaderPackages() | JVMCI-aware boot loader package access. |
Class.isAssignableFrom(Class) | ResolvedJavaType.isAssignableFrom(ResolvedJavaType) or ReflectionUtil.isAssignableFrom(Class, Class) | Use ResolvedJavaType for runtime or guest types; ReflectionUtil is acceptable for known hosted literals when satisfying VerifyReflectionUsage. |
ReflectionUtil.invokeMethod(...), Method.invoke(...), and ordinary reflective invocation | GuestAccess.get().invoke(...) | Preferred invocation path for guest methods. |
Under Terminus, code frequently operates on guest-loaded types that do not have a reliable or meaningful host Class<?> counterpart.
That means reflection-based code is often either incorrect or only accidentally correct while running in host mode.
Use:
GuestAccess.get() for guest-context operationsJVMCIReflectionUtil for reflection-like helpersResolvedJavaType, ResolvedJavaMethod, and ResolvedJavaField for type and member reasoningSeveral JVMCI replacements are intentionally stricter than reflection lookups.
For example, unique-declaration helpers can return null if a method name and parameter list do not resolve unambiguously.
Migration work should handle that explicitly rather than assuming reflective lookup semantics.
The migration guide treats module and package APIs as part of the same transition.
If code currently reasons in terms of Module, ModuleLayer, or Package, move it toward:
ResolvedJavaModuleResolvedJavaModuleLayerResolvedJavaPackageJVMCIReflectionUtil.bootModuleLayer()If existing hosted code uses Method.invoke(...) or helper wrappers around core reflection, replace that with GuestAccess.get().invoke(...) so the invocation stays within the intended guest-context model.
JVMCIReflectionUtil or JVMCI metadata APIs.ResolvedJavaType.isAssignableFrom(...) when dealing with guest/runtime types.GuestAccess.get().invoke(...).null instead of assuming reflection-style success.