From 2bd60629ce0e548d38031e17123c7a345b61e0dd Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 17 Sep 2026 13:59:39 +0200 Subject: [PATCH] wip --- eng/native.wasm.targets | 1 + eng/testing/tests.browser.targets | 47 ++++++ src/coreclr/inc/webcildecoder.h | 7 +- .../CompilationModuleGroup.ReadyToRun.cs | 9 ++ .../ReadyToRun/ModuleTokenResolver.cs | 24 ++- .../ReadyToRun/SignatureBuilder.cs | 4 +- .../ReadyToRunCodegenNodeFactory.cs | 4 + .../Compiler/ReadyToRunCodegenCompilation.cs | 2 +- .../ReadyToRunCompilationModuleGroupBase.cs | 27 +++- ...RunSingleAssemblyCompilationModuleGroup.cs | 24 ++- .../aot/crossgen2/Crossgen2RootCommand.cs | 12 ++ src/coreclr/tools/aot/crossgen2/Program.cs | 15 +- .../aot/crossgen2/Properties/Resources.resx | 12 ++ src/coreclr/vm/ceeload.cpp | 102 +++++++++++-- src/coreclr/vm/ceeload.h | 36 ++++- src/coreclr/vm/ceeload.inl | 12 +- src/coreclr/vm/codeman.cpp | 24 ++- src/coreclr/vm/codeman.h | 26 ++-- src/coreclr/vm/codeman.inl | 2 +- .../vm/datadescriptor/datadescriptor.inc | 2 +- src/coreclr/vm/frames.cpp | 20 ++- src/coreclr/vm/jitinterface.cpp | 52 ++++--- src/coreclr/vm/jitinterface.h | 8 +- src/coreclr/vm/nativeimage.cpp | 118 +++++++++++++- src/coreclr/vm/nativeimage.h | 12 ++ src/coreclr/vm/prestub.cpp | 73 ++++++++- src/coreclr/vm/readytoruninfo.cpp | 87 ++++++++++- src/coreclr/vm/readytoruninfo.h | 17 ++- src/coreclr/vm/siginfo.cpp | 2 +- src/coreclr/vm/wasm/helpers.cpp | 105 ++++++++++++- src/coreclr/vm/zapsig.cpp | 53 ++++--- src/coreclr/vm/zapsig.h | 36 +++-- .../build/BrowserWasmApp.CoreCLR.targets | 1 + ...ET.Sdk.WebAssembly.Browser.CoreCLR.targets | 144 +++++++++++++++++- .../libs/Common/JavaScript/host/assets.ts | 36 ++++- .../libs/Common/JavaScript/loader/assets.ts | 37 +++++ .../libs/Common/JavaScript/loader/dotnet.d.ts | 2 + .../libs/Common/JavaScript/loader/run.ts | 5 +- .../Common/JavaScript/types/emscripten.ts | 2 + .../Common/JavaScript/types/public-api.ts | 3 + .../StackWalk/Context/Wasm/WasmR2RInfo.cs | 5 +- .../Data/FunctionTableIndexRangeSection.cs | 2 +- .../cdac/tests/UnitTests/WasmR2RInfoTests.cs | 8 +- .../BootJsonBuilderHelper.cs | 46 ++++++ .../BootJsonData.cs | 17 +++ .../GenerateWasmBootJson.cs | 8 + .../WasmInterpreterTransitions.cs | 49 ++++++ 47 files changed, 1184 insertions(+), 156 deletions(-) diff --git a/eng/native.wasm.targets b/eng/native.wasm.targets index c72c24ff6df2d1..d6be4a89d47f21 100644 --- a/eng/native.wasm.targets +++ b/eng/native.wasm.targets @@ -146,6 +146,7 @@ + <_EmccExportedRuntimeMethods>@(EmccExportedRuntimeMethod -> '%(Identity)',',') diff --git a/eng/testing/tests.browser.targets b/eng/testing/tests.browser.targets index 0276d1a0ec8aea..a969ccc5a8ed37 100644 --- a/eng/testing/tests.browser.targets +++ b/eng/testing/tests.browser.targets @@ -93,6 +93,53 @@ + + + + <_WasmLazyR2RTestStagedFile Include="$(_WasmPublishR2RDir)*.r2r.wasm" /> + <_WasmLazyR2RTestCandidate Include="@(_WasmLazyR2RTestStagedFile)"> + $(PackageId) + Computed + $(_WasmPublishR2RDir) + $(StaticWebAssetBasePath) + _framework/%(FileName)%(Extension) + All + All + Primary + WasmResource + lazy-r2r + $(_WasmFrameworkCopyToOutputDirectory) + PreserveNewest + %(Identity) + + + <_WasmLazyR2RTestFingerprintPatterns Include="WasmLazyR2R" Pattern="*.wasm" Expression="#[.{fingerprint}]!" Condition="'$(_WasmFingerprintAssets)' == 'true'" /> + <_WasmLazyR2RTestFingerprintPatterns Include="WasmLazyR2R" Pattern="*.wasm" Expression="#[.{fingerprint}]?" Condition="'$(_WasmFingerprintAssets)' != 'true'" /> + + + + + + + + + true true diff --git a/src/coreclr/inc/webcildecoder.h b/src/coreclr/inc/webcildecoder.h index 18b74c65a4a5e8..422aeed0d5fef8 100644 --- a/src/coreclr/inc/webcildecoder.h +++ b/src/coreclr/inc/webcildecoder.h @@ -199,7 +199,6 @@ class WebcilDecoder // R2R // ------------------------------------------------------------ - BOOL HasReadyToRunHeader() const; BOOL IsComponentAssembly() const { // A webcil composite's component assemblies carry the R2R COMPONENT flag; the flat FALSE @@ -207,7 +206,6 @@ class WebcilDecoder // report every webcil image as non-component, disabling composite R2R load. return HasReadyToRunHeader() && (GetReadyToRunHeader()->CoreHeader.Flags & READYTORUN_FLAG_COMPONENT) != 0; } - READYTORUN_HEADER *GetReadyToRunHeader() const; BOOL IsNativeMachineFormat() const { return true; } // This can only be loaded on a Wasm runtime which matches the necessary load environment, which means these are always in the native machine format. PTR_CVOID GetNativeManifestMetadata(COUNT_T *pSize) const; @@ -221,6 +219,11 @@ class WebcilDecoder return 0; return m_pHeader->VersionMajor >= WEBCIL_VERSION_MAJOR_1 ? ((const WebcilHeader_1 *)m_pHeader)->TableBase : 0; } + + // Public so a memory-resident webcil composite (attached lazy R2R supplement) can obtain its + // R2R header without a PEImageLayout. + BOOL HasReadyToRunHeader() const; + READYTORUN_HEADER *GetReadyToRunHeader() const; private: READYTORUN_HEADER *FindReadyToRunHeader() const; public: diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/CompilationModuleGroup.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/CompilationModuleGroup.ReadyToRun.cs index d687b2b2862fa7..c46ceb7240c3dd 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/CompilationModuleGroup.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/CompilationModuleGroup.ReadyToRun.cs @@ -66,6 +66,15 @@ partial class CompilationModuleGroup /// True if the given module versions with the current compilation module group public abstract bool VersionsWithModule(ModuleDesc module); + /// + /// Returns true when a cross-module type reference to the given module should be encoded as the + /// defining module's typeDef (a direct, version-specific metadata token) rather than a version-resilient + /// manifest reference. Only valid when the referenced modules form a closed, MVID-locked deployment. + /// + /// Defining module of the referenced type + /// True if the type reference may be hard-bound to the defining module's token + public virtual bool HardBindTypeReference(ModuleDesc module) => false; + /// /// Checks if the given PInvoke method can produce a PInvoke stub in the current compilation, depending on the method's /// signature and the compilation policy. diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ModuleTokenResolver.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ModuleTokenResolver.cs index 03b9b1e9864996..0369191188a6ad 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ModuleTokenResolver.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ModuleTokenResolver.cs @@ -63,6 +63,14 @@ public ModuleToken GetModuleTokenForType(TypeDesc type, bool allowDynamicallyCre return new ModuleToken(ecmaType.Module, (mdToken)MetadataTokens.GetToken(ecmaType.Handle)); } + // Closed-deployment (wasm split) builds bind cross-module type identity to the defining + // module's typeDef so the runtime resolves it by a direct metadata lookup against the + // attached image, rather than a version-resilient manifest name-ref. + if (_compilationModuleGroup.HardBindTypeReference(ecmaType.Module)) + { + return new ModuleToken(ecmaType.Module, (mdToken)MetadataTokens.GetToken(ecmaType.Handle)); + } + if (_typeToRefTokens.TryGetValue(ecmaType, out token)) { return token; @@ -108,6 +116,13 @@ public ModuleToken GetModuleTokenForMethod(MethodDesc method, bool allowDynamica return new ModuleToken(ecmaMethod.Module, ecmaMethod.Handle); } + // Closed-deployment (wasm split) builds bind the method to its defining module's methodDef + // so the runtime (and the owner type derived from it) resolves by a direct metadata lookup. + if (_compilationModuleGroup.HardBindTypeReference(ecmaMethod.Module)) + { + return new ModuleToken(ecmaMethod.Module, ecmaMethod.Handle); + } + // If that didn't work, it may be in the manifest module used for version resilient cross module inlining if (allowDynamicallyCreatedReference) { @@ -139,6 +154,12 @@ public ModuleToken GetModuleTokenForField(FieldDesc field, bool allowDynamically return new ModuleToken(ecmaField.Module, ecmaField.Handle); } + // Closed-deployment (wasm split) builds bind the field to its defining module's fieldDef. + if (_compilationModuleGroup.HardBindTypeReference(ecmaField.Module)) + { + return new ModuleToken(ecmaField.Module, ecmaField.Handle); + } + // If that didn't work, it may be in the manifest module used for version resilient cross module inlining if (allowDynamicallyCreatedReference) { @@ -347,7 +368,8 @@ public int GetModuleIndex(IEcmaModule module) int moduleIndex = _moduleIndexLookup(module); if (moduleIndex != 0 && !(module is Internal.TypeSystem.Ecma.MutableModule)) { - if (!_compilationModuleGroup.VersionsWithModule((ModuleDesc)module)) + if (!_compilationModuleGroup.VersionsWithModule((ModuleDesc)module) && + !_compilationModuleGroup.HardBindTypeReference((ModuleDesc)module)) { throw new InternalCompilerErrorException("Attempt to use token from a module not within the version bubble"); } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/SignatureBuilder.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/SignatureBuilder.cs index 0c82536d8535a6..0744611ab3006b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/SignatureBuilder.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/SignatureBuilder.cs @@ -611,7 +611,9 @@ public SignatureContext EmitFixup(NodeFactory factory, ReadyToRunFixupKind fixup else { EmitByte((byte)(fixupKind | ReadyToRunFixupKind.ModuleOverride)); - if (!(targetModule is Internal.TypeSystem.Ecma.MutableModule) && !factory.CompilationModuleGroup.VersionsWithModule((ModuleDesc)targetModule)) + if (!(targetModule is Internal.TypeSystem.Ecma.MutableModule) && + !factory.CompilationModuleGroup.VersionsWithModule((ModuleDesc)targetModule) && + !factory.CompilationModuleGroup.HardBindTypeReference((ModuleDesc)targetModule)) { throw new InternalCompilerErrorException("Attempt to use token from a module not within the version bubble"); } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs index 978adbe6d9c650..d53ef44a1d42f0 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs @@ -71,6 +71,10 @@ public struct NodeFactoryOptimizationFlags public bool StripInliningInfo; public bool StripDebugInfo; public bool StripILBodies; + // Composite mode only: emit the composite image but skip rewriting each input into a component + // stub. Used for the lazy half of the browser-wasm R2R split, where the eager standalone image + // already serves as the component and the composite carries only the complement native code. + public bool SuppressComponentRewrite; public HashSet CompiledMethodDefs; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs index 291676747216ed..bc559942374986 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs @@ -443,7 +443,7 @@ public override void Compile(string outputFile) _logger); CompilationModuleGroup moduleGroup = _nodeFactory.CompilationModuleGroup; - if (moduleGroup.IsCompositeBuildMode) + if (moduleGroup.IsCompositeBuildMode && !_nodeFactory.OptimizationFlags.SuppressComponentRewrite) { // In composite mode with standalone MSIL we rewrite all input MSIL assemblies to the // output folder, adding a formal R2R header to them with forwarding information to diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs index 3b5eb16758a700..f01b89c983a235 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs @@ -19,6 +19,15 @@ namespace ILCompiler { + // Selects which methods a profile-restricted (partial) R2R compilation includes. The complement + // mode is used to produce the lazy half of a split image: the native code the profile did NOT select. + public enum ProfileRestrictionMode + { + None, // No restriction: compile everything (the profile only guides optimization). + ProfileOnly, // Compile only methods present in the input profile (the classic --partial image). + ProfileComplement, // Compile only methods NOT present in the input profile. + } + public class ReadyToRunCompilationModuleGroupConfig { public CompilerTypeSystemContext Context; @@ -32,6 +41,8 @@ public class ReadyToRunCompilationModuleGroupConfig public bool CompileGenericDependenciesFromVersionBubbleModuleSet; public bool CompileAllPossibleCrossModuleCode; public InstructionSetSupport InstructionSetSupport; + public bool KeepIntrinsicMethodsInPartial; + public bool HardBindCrossModuleTypeReferences; } public abstract class ReadyToRunCompilationModuleGroupBase : CompilationModuleGroup @@ -67,6 +78,18 @@ public abstract class ReadyToRunCompilationModuleGroupBase : CompilationModuleGr private readonly Func _tokenTranslationFreeNonVersionableUncached; private bool CompileAllPossibleCrossModuleCode = false; private InstructionSetSupport _instructionSetSupport; + private readonly bool _keepIntrinsicMethodsInPartial; + private readonly bool _hardBindCrossModuleTypeReferences; + + // On an interpreter-only target, a --partial image must still carry hardware-intrinsic methods so the + // interpreter can call their R2R fallback instead of emitting INTOP_THROW_PNSE. See --partial-keep-intrinsic-methods. + protected bool KeepIntrinsicMethodsInPartial => _keepIntrinsicMethodsInPartial; + + // When the referenced assemblies are a closed, MVID-locked deployment (wasm per-assembly split R2R), + // cross-module type references are encoded as defining-module typeDefs (a direct metadata lookup the + // runtime resolves against the attached image) instead of version-resilient manifest name-refs. This + // binds only type IDENTITY by token; layout/inlining stay version-resilient (VersionsWithModule is unchanged). + public override bool HardBindTypeReference(ModuleDesc module) => _hardBindCrossModuleTypeReferences && module is EcmaModule; public ReadyToRunCompilationModuleGroupBase(ReadyToRunCompilationModuleGroupConfig config) { @@ -77,6 +100,8 @@ public ReadyToRunCompilationModuleGroupBase(ReadyToRunCompilationModuleGroupConf _crossModuleInlining = config.CrossModuleInlining; _crossModuleGenericCompilation = config.CrossModuleGenericCompilation; CompileAllPossibleCrossModuleCode = config.CompileAllPossibleCrossModuleCode; + _keepIntrinsicMethodsInPartial = config.KeepIntrinsicMethodsInPartial; + _hardBindCrossModuleTypeReferences = config.HardBindCrossModuleTypeReferences; Debug.Assert(_isCompositeBuildMode || _compilationModuleSet.Count == 1); @@ -947,7 +972,7 @@ static bool ComputeInstantiationTypeVersionsWithCode(Func versio } } - public virtual void ApplyProfileGuidedOptimizationData(ProfileDataManager profileGuidedCompileRestriction, bool makePartial) + public virtual void ApplyProfileGuidedOptimizationData(ProfileDataManager profileGuidedCompileRestriction, ProfileRestrictionMode mode) { _profileData = profileGuidedCompileRestriction; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunSingleAssemblyCompilationModuleGroup.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunSingleAssemblyCompilationModuleGroup.cs index add2cbc8e43838..2468cda2df92d1 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunSingleAssemblyCompilationModuleGroup.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunSingleAssemblyCompilationModuleGroup.cs @@ -14,6 +14,7 @@ namespace ILCompiler public class ReadyToRunSingleAssemblyCompilationModuleGroup : ReadyToRunCompilationModuleGroupBase { private ProfileDataManager _profileGuidedCompileRestriction; + private ProfileRestrictionMode _profileRestrictionMode; private bool _profileGuidedCompileRestrictionSet; public ReadyToRunSingleAssemblyCompilationModuleGroup( @@ -29,9 +30,19 @@ protected sealed override bool ContainsMethodBodyCore(MethodDesc method, bool un if (_profileGuidedCompileRestriction != null) { - if (!_profileGuidedCompileRestriction.IsMethodInInputProfileData(method) && - (((ReadyToRunCompilerContext)method.Context).TargetAllowsRuntimeCodeGeneration || - !HardwareIntrinsicHelpers.IsHardwareIntrinsic(method))) + bool inProfile = _profileGuidedCompileRestriction.IsMethodInInputProfileData(method); + // ProfileComplement keeps the methods the profile did NOT select; ProfileOnly keeps those it did. + bool keep = (_profileRestrictionMode == ProfileRestrictionMode.ProfileComplement) ? !inProfile : inProfile; + if (HardwareIntrinsicHelpers.IsHardwareIntrinsic(method) && + (KeepIntrinsicMethodsInPartial || + !((ReadyToRunCompilerContext)method.Context).TargetAllowsRuntimeCodeGeneration)) + { + // Hardware-intrinsic methods must live in the eager (ProfileOnly) image: either because the + // target can't JIT them at runtime, or so an interpreter-only startup has an R2R fallback to + // call. Keep them there and out of the complement to avoid duplication. + keep = (_profileRestrictionMode == ProfileRestrictionMode.ProfileOnly); + } + if (!keep) return false; } @@ -45,16 +56,17 @@ protected sealed override bool ContainsMethodBodyCore(MethodDesc method, bool un return (ContainsType(method.OwningType) && VersionsWithMethodBody(method)) || CompileVersionBubbleGenericsIntoCurrentModule(method) || this.CrossModuleCompileable(method); } - public sealed override void ApplyProfileGuidedOptimizationData(ProfileDataManager profileGuidedCompileRestriction, bool partial) + public sealed override void ApplyProfileGuidedOptimizationData(ProfileDataManager profileGuidedCompileRestriction, ProfileRestrictionMode mode) { if (_profileGuidedCompileRestrictionSet) throw new InternalCompilerErrorException("Called ApplyProfileGuidedOptimizationData twice."); _profileGuidedCompileRestrictionSet = true; - if (partial) + _profileRestrictionMode = mode; + if (mode != ProfileRestrictionMode.None) _profileGuidedCompileRestriction = profileGuidedCompileRestriction; - base.ApplyProfileGuidedOptimizationData(profileGuidedCompileRestriction, partial); + base.ApplyProfileGuidedOptimizationData(profileGuidedCompileRestriction, mode); } public override ReadyToRunFlags GetReadyToRunFlags() diff --git a/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs b/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs index 82111b81b11efc..22f7d0283e8d49 100644 --- a/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs +++ b/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs @@ -66,6 +66,14 @@ internal class Crossgen2RootCommand : RootCommand new("--single-file-compilation") { Description = SR.SingleFileCompilationOption }; public Option Partial { get; } = new("--partial") { Description = SR.PartialImageOption }; + public Option PartialComplement { get; } = + new("--partial-complement") { Description = SR.PartialComplementOption }; + public Option PartialKeepIntrinsicMethods { get; } = + new("--partial-keep-intrinsic-methods") { Description = SR.PartialKeepIntrinsicMethodsOption }; + public Option SuppressComponentRewrite { get; } = + new("--suppress-component-rewrite") { Description = SR.SuppressComponentRewriteOption }; + public Option HardBindCrossModuleTypeReferences { get; } = + new("--hard-cross-module-type-refs") { Description = SR.HardBindCrossModuleTypeReferencesOption }; public Option CompileBubbleGenerics { get; } = new("--compilebubblegenerics") { Description = SR.BubbleGenericsOption }; public Option EmbedPgoData { get; } = @@ -195,6 +203,10 @@ public Crossgen2RootCommand(string[] args) : base(SR.Crossgen2BannerText) Options.Add(OutNearInput); Options.Add(SingleFileCompilation); Options.Add(Partial); + Options.Add(PartialComplement); + Options.Add(PartialKeepIntrinsicMethods); + Options.Add(SuppressComponentRewrite); + Options.Add(HardBindCrossModuleTypeReferences); Options.Add(CompileBubbleGenerics); Options.Add(EmbedPgoData); Options.Add(DgmlLogFileName); diff --git a/src/coreclr/tools/aot/crossgen2/Program.cs b/src/coreclr/tools/aot/crossgen2/Program.cs index 13c6768db5be52..e6f12e9c682211 100644 --- a/src/coreclr/tools/aot/crossgen2/Program.cs +++ b/src/coreclr/tools/aot/crossgen2/Program.cs @@ -485,6 +485,8 @@ private void RunSingleCompilation(Dictionary inFilePaths, Instru groupConfig.CrossModuleInlineable = crossModuleInlineableCode; groupConfig.CompileAllPossibleCrossModuleCode = false; groupConfig.InstructionSetSupport = instructionSetSupport; + groupConfig.KeepIntrinsicMethodsInPartial = Get(_command.PartialKeepIntrinsicMethods); + groupConfig.HardBindCrossModuleTypeReferences = Get(_command.HardBindCrossModuleTypeReferences); // Handle non-local generics command line option ModuleDesc nonLocalGenericsHome = compileBubbleGenerics ? inputModules[0] : null; @@ -593,7 +595,12 @@ private void RunSingleCompilation(Dictionary inFilePaths, Instru Get(_command.SynthesizeRandomMibc)); bool partial = Get(_command.Partial); - compilationGroup.ApplyProfileGuidedOptimizationData(profileDataManager, partial); + bool partialComplement = Get(_command.PartialComplement); + ProfileRestrictionMode profileMode = + partialComplement ? ProfileRestrictionMode.ProfileComplement : + partial ? ProfileRestrictionMode.ProfileOnly : + ProfileRestrictionMode.None; + compilationGroup.ApplyProfileGuidedOptimizationData(profileDataManager, profileMode); if ((singleMethod == null) && !compileNoMethods) { @@ -601,8 +608,9 @@ private void RunSingleCompilation(Dictionary inFilePaths, Instru foreach (var module in rootingModules) { compilationRoots.Add(new ReadyToRunProfilingRootProvider(module, profileDataManager)); - // If we're doing partial precompilation, only use profile data. - if (!partial) + // ProfileOnly roots from the profile alone; None and ProfileComplement need the + // full closure reachable so ContainsMethodBody can select the complement. + if (profileMode != ProfileRestrictionMode.ProfileOnly) { if (ReadyToRunVisibilityRootProvider.UseVisibilityBasedRootProvider(module)) { @@ -686,6 +694,7 @@ private void RunSingleCompilation(Dictionary inFilePaths, Instru nodeFactoryFlags.StripInliningInfo = Get(_command.StripInliningInfo); nodeFactoryFlags.StripDebugInfo = Get(_command.StripDebugInfo); nodeFactoryFlags.StripILBodies = Get(_command.StripILBodies); + nodeFactoryFlags.SuppressComponentRewrite = Get(_command.SuppressComponentRewrite); builder .UseMapFile(Get(_command.Map)) diff --git a/src/coreclr/tools/aot/crossgen2/Properties/Resources.resx b/src/coreclr/tools/aot/crossgen2/Properties/Resources.resx index 41cbb9df6a5ae7..2da11bd194b021 100644 --- a/src/coreclr/tools/aot/crossgen2/Properties/Resources.resx +++ b/src/coreclr/tools/aot/crossgen2/Properties/Resources.resx @@ -159,6 +159,9 @@ True to skip compiling methods into the R2R image (default = false) + + In a --partial image, still compile hardware-intrinsic methods so an interpreter-only startup (no JIT) has an R2R fallback to call instead of throwing PlatformNotSupportedException (default = false) + Emit a composite R2R image comprising a number of input assemblies @@ -237,6 +240,15 @@ Generate partial image driven by profile + + Generate an image with only the methods NOT selected by the profile (the complement of --partial) + + + In composite mode, emit the composite image but do not rewrite inputs into component stubs + + + Encode cross-module type references as the defining module's typeDef token instead of a version-resilient manifest reference. Only valid when the referenced assemblies form a closed, MVID-locked deployment such as wasm per-assembly split R2R (default = false) + Error: {0} diff --git a/src/coreclr/vm/ceeload.cpp b/src/coreclr/vm/ceeload.cpp index bae6a7593dfeac..282373a8fa292a 100644 --- a/src/coreclr/vm/ceeload.cpp +++ b/src/coreclr/vm/ceeload.cpp @@ -70,6 +70,9 @@ #include "../md/compiler/custattr.h" #include "typekey.h" #include "peimagelayout.inl" +#ifdef TARGET_WASM +#include "pregeneratedstringthunks.h" +#endif #include "interpexec.h" @@ -459,6 +462,7 @@ void Module::Initialize(AllocMemTracker *pamTracker, LPCWSTR szName) #ifdef FEATURE_READYTORUN m_pNativeImage = NULL; + m_pSupplementalReadyToRunInfos = NULL; if ((m_pReadyToRunInfo = ReadyToRunInfo::Initialize(this, pamTracker)) != NULL) { if (m_pReadyToRunInfo->SkipTypeValidation()) @@ -3401,7 +3405,7 @@ void Module::FixupVTables() } #endif // FEATURE_IJW -ModuleBase *Module::GetModuleFromIndex(DWORD ix) +ModuleBase *Module::GetModuleFromIndex(DWORD ix, ReadyToRunInfo *pInfo) { CONTRACTL { @@ -3412,9 +3416,9 @@ ModuleBase *Module::GetModuleFromIndex(DWORD ix) } CONTRACTL_END; - if (IsReadyToRun()) + if (IsReadyToRun() || pInfo != NULL) { - return ZapSig::DecodeModuleFromIndex(this, ix); + return ZapSig::DecodeModuleFromIndex(this, ix, pInfo); } else { @@ -3434,7 +3438,7 @@ ModuleBase *Module::GetModuleFromIndex(DWORD ix) #endif // !DACCESS_COMPILE -ModuleBase *Module::GetModuleFromIndexIfLoaded(DWORD ix) +ModuleBase *Module::GetModuleFromIndexIfLoaded(DWORD ix, ReadyToRunInfo *pInfo) { CONTRACTL { @@ -3447,7 +3451,7 @@ ModuleBase *Module::GetModuleFromIndexIfLoaded(DWORD ix) CONTRACTL_END; #ifndef DACCESS_COMPILE - return ZapSig::DecodeModuleFromIndexIfLoaded(this, ix); + return ZapSig::DecodeModuleFromIndexIfLoaded(this, ix, pInfo); #else // DACCESS_COMPILE DacNotImpl(); return NULL; @@ -3534,7 +3538,7 @@ void Module::RunEagerFixups() // from multiple threads so we need to lock their resolution. CrstHolder compositeEagerFixups(compositeNativeImage->EagerFixupsLock()); #ifdef TARGET_WASM - GetReadyToRunInfo()->RegisterVirtualIPRange(this); + GetReadyToRunInfo()->RegisterVirtualIPRange(); if (nSections == 0) return; #endif // TARGET_WASM @@ -3554,7 +3558,7 @@ void Module::RunEagerFixups() { // Per-module eager fixups don't need locking #ifdef TARGET_WASM - GetReadyToRunInfo()->RegisterVirtualIPRange(this); + GetReadyToRunInfo()->RegisterVirtualIPRange(); if (nSections == 0) return; #endif // TARGET_WASM @@ -3618,9 +3622,85 @@ void Module::RunEagerFixupsUnlocked() #ifndef DACCESS_COMPILE +#ifdef FEATURE_READYTORUN +// Attach a lazily-downloaded supplemental R2R image to this module. Lock-free push onto the list +// head; attach runs at a quiesce point so contention is not expected but the CAS keeps it safe. +void Module::AttachSupplementalReadyToRunInfo(ReadyToRunInfo *pInfo) +{ + STANDARD_VM_CONTRACT; + _ASSERTE(pInfo != NULL); + _ASSERTE(pInfo->GetNextSupplemental() == NULL); + + PTR_ReadyToRunInfo pOld; + do + { + pOld = VolatileLoadWithoutBarrier(&m_pSupplementalReadyToRunInfos); + pInfo->SetNextSupplemental(pOld); + } while (InterlockedCompareExchangeT(&m_pSupplementalReadyToRunInfos, PTR_ReadyToRunInfo(pInfo), pOld) != pOld); +} + +#ifdef TARGET_WASM +void Module::RunSupplementalEagerFixups(ReadyToRunInfo *pInfo) +{ + STANDARD_VM_CONTRACT; + _ASSERTE(pInfo != NULL); + + COUNT_T nSections; + PTR_READYTORUN_IMPORT_SECTION pSections = pInfo->GetImportSections(&nSections); + ReadyToRunLoadedImage *pImage = pInfo->GetImage(); + + for (COUNT_T iSection = 0; iSection < nSections; iSection++) + { + PTR_READYTORUN_IMPORT_SECTION pSection = pSections + iSection; + if ((pSection->Flags & ReadyToRunImportSectionFlags::Eager) != ReadyToRunImportSectionFlags::Eager) + continue; + + COUNT_T tableSize; + TADDR tableBase = pImage->GetDirectoryData(&pSection->Section, &tableSize); + PTR_DWORD pSignatures = dac_cast(pImage->GetRvaData(pSection->Signatures)); + + for (SIZE_T *fixupCell = (SIZE_T *)tableBase; fixupCell < (SIZE_T *)(tableBase + tableSize); fixupCell++) + { + SIZE_T fixupIndex = fixupCell - (SIZE_T *)tableBase; + PCCOR_SIGNATURE pBlob = (PCCOR_SIGNATURE)pImage->GetRvaData(pSignatures[fixupIndex]); + BYTE kind = *pBlob++; + if (kind & READYTORUN_FIXUP_ModuleOverride) + { + CorSigUncompressData(pBlob); + kind &= ~READYTORUN_FIXUP_ModuleOverride; + } + + // The string-thunk fixup is image-specific: it must register the supplemental image's thunks + // against the supplemental info rather than resolve a value into the cell, so handle it here. + if (kind == READYTORUN_FIXUP_InjectStringThunks) + { + ProcessInjectStringThunksFixup(pInfo, pBlob); + VolatileStore(fixupCell, (SIZE_T)1); + continue; + } + + // Every other eager fixup (helpers, instruction-set checks, eager type/method handles, ...) + // must be resolved now, exactly as Module::RunEagerFixupsUnlocked does for the primary image; + // an unresolved eager helper cell is a null indirection that faults when the attached R2R + // code first calls through it. Route the signature blob through the supplemental image. + if (!LoadDynamicInfoEntry(this, pSignatures[fixupIndex], fixupCell, TRUE /* mayUsePrecompiledPInvokeMethods */, pInfo)) + { + // A failed eager check (e.g. an unsupported instruction set) means this image's native + // code cannot be used; disable it and keep running on the interpreter. Best-effort: + // the eager image already provided a working (partial or IL-only) module. + pInfo->DisableAllR2RCode(); + return; + } + _ASSERTE(*fixupCell != 0); + } + } +} +#endif // TARGET_WASM +#endif // FEATURE_READYTORUN + //----------------------------------------------------------------------------- -BOOL Module::FixupNativeEntry(READYTORUN_IMPORT_SECTION* pSection, SIZE_T fixupIndex, SIZE_T* fixupCell, BOOL mayUsePrecompiledPInvokeMethods) +BOOL Module::FixupNativeEntry(READYTORUN_IMPORT_SECTION* pSection, SIZE_T fixupIndex, SIZE_T* fixupCell, BOOL mayUsePrecompiledPInvokeMethods, ReadyToRunInfo * pInfo) { CONTRACTL { @@ -3634,9 +3714,11 @@ BOOL Module::FixupNativeEntry(READYTORUN_IMPORT_SECTION* pSection, SIZE_T fixupI if (fixup == 0) { - PTR_DWORD pSignatures = dac_cast(GetReadyToRunImage()->GetRvaData(pSection->Signatures)); + // A supplemental (lazily-attached) image stores its signatures in its own image; resolve against it. + ReadyToRunLoadedImage * pNativeImage = (pInfo != NULL) ? pInfo->GetImage() : GetReadyToRunImage(); + PTR_DWORD pSignatures = dac_cast(pNativeImage->GetRvaData(pSection->Signatures)); - if (!LoadDynamicInfoEntry(this, pSignatures[fixupIndex], fixupCell, mayUsePrecompiledPInvokeMethods)) + if (!LoadDynamicInfoEntry(this, pSignatures[fixupIndex], fixupCell, mayUsePrecompiledPInvokeMethods, pInfo)) return FALSE; _ASSERTE(*fixupCell != 0); diff --git a/src/coreclr/vm/ceeload.h b/src/coreclr/vm/ceeload.h index 91ccbe5ad5dac5..eaa4ce1b4fe78b 100644 --- a/src/coreclr/vm/ceeload.h +++ b/src/coreclr/vm/ceeload.h @@ -853,6 +853,10 @@ class Module : public ModuleBase private: PTR_ReadyToRunInfo m_pReadyToRunInfo; PTR_NativeImage m_pNativeImage; + // Head of a singly-linked list of supplemental R2R images attached to this module after load + // (lazily downloaded native code for methods whose metadata/IL live in this module's primary + // image). Linked through ReadyToRunInfo::m_pNextSupplemental. Empty for all modules today. + PTR_ReadyToRunInfo m_pSupplementalReadyToRunInfos; #endif #if PROFILING_SUPPORTED_DATA @@ -1493,22 +1497,32 @@ class Module : public ModuleBase IMDInternalImport *GetNativeAssemblyImport(BOOL loadAllowed = TRUE); IMDInternalImport *GetNativeAssemblyImportIfLoaded(); - BOOL FixupNativeEntry(READYTORUN_IMPORT_SECTION * pSection, SIZE_T fixupIndex, SIZE_T *fixup, BOOL mayUsePrecompiledPInvokeMethods = TRUE); + BOOL FixupNativeEntry(READYTORUN_IMPORT_SECTION * pSection, SIZE_T fixupIndex, SIZE_T *fixup, BOOL mayUsePrecompiledPInvokeMethods = TRUE, ReadyToRunInfo * pInfo = NULL); //this split exists to support new CLR Dump functionality in DAC. The //template removes any indirections. - BOOL FixupDelayList(TADDR pFixupList, BOOL mayUsePrecompiledPInvokeMethods = TRUE); + // pInfo, when non-NULL, resolves the fixups against that (supplemental) R2R image instead of the + // module's primary image. + BOOL FixupDelayList(TADDR pFixupList, BOOL mayUsePrecompiledPInvokeMethods = TRUE, ReadyToRunInfo * pInfo = NULL); template BOOL FixupDelayListAux(TADDR pFixupList, Ptr pThis, FixupNativeEntryCallback pfnCB, PTR_READYTORUN_IMPORT_SECTION pImportSections, COUNT_T nImportSections, - ReadyToRunLoadedImage * pNativeImage, BOOL mayUsePrecompiledPInvokeMethods = TRUE); + ReadyToRunLoadedImage * pNativeImage, BOOL mayUsePrecompiledPInvokeMethods = TRUE, + ReadyToRunInfo * pInfo = NULL); void RunEagerFixups(); void RunEagerFixupsUnlocked(); +#ifdef TARGET_WASM + // Run the eager fixups of a lazily-attached supplemental R2R image (as opposed to this module's + // primary image), resolving them against that image rather than the module's primary one. + void RunSupplementalEagerFixups(ReadyToRunInfo *pInfo); +#endif - ModuleBase *GetModuleFromIndex(DWORD ix); - ModuleBase *GetModuleFromIndexIfLoaded(DWORD ix); + // pInfo, when non-NULL, resolves the index against that supplemental R2R image's manifest rather than + // this module's primary R2R info (for lazily-attached supplemental images). + ModuleBase *GetModuleFromIndex(DWORD ix, ReadyToRunInfo *pInfo = NULL); + ModuleBase *GetModuleFromIndexIfLoaded(DWORD ix, ReadyToRunInfo *pInfo = NULL); BOOL IsReadyToRun() const { @@ -1539,6 +1553,18 @@ class Module : public ModuleBase LIMITED_METHOD_DAC_CONTRACT; return m_pNativeImage; } + + PTR_ReadyToRunInfo GetSupplementalReadyToRunInfos() const + { + LIMITED_METHOD_DAC_CONTRACT; + return m_pSupplementalReadyToRunInfos; + } + +#ifndef DACCESS_COMPILE + // Attach a lazily-downloaded supplemental R2R image to this module. Callable only at a quiesce + // point (no managed frames of this module on the stack). Lock-free push onto the list head. + void AttachSupplementalReadyToRunInfo(ReadyToRunInfo *pInfo); +#endif #endif #ifdef _DEBUG diff --git a/src/coreclr/vm/ceeload.inl b/src/coreclr/vm/ceeload.inl index 1b4204d8de96fc..8ff78b358f34b0 100644 --- a/src/coreclr/vm/ceeload.inl +++ b/src/coreclr/vm/ceeload.inl @@ -350,21 +350,23 @@ inline mdAssemblyRef Module::FindAssemblyRef(Assembly *targetAssembly) #include "nibblestream.h" -FORCEINLINE BOOL Module::FixupDelayList(TADDR pFixupList, BOOL mayUsePrecompiledPInvokeMethods) +FORCEINLINE BOOL Module::FixupDelayList(TADDR pFixupList, BOOL mayUsePrecompiledPInvokeMethods, ReadyToRunInfo * pInfo) { WRAPPER_NO_CONTRACT; COUNT_T nImportSections; - PTR_READYTORUN_IMPORT_SECTION pImportSections = GetImportSections(&nImportSections); + PTR_READYTORUN_IMPORT_SECTION pImportSections = (pInfo != NULL) ? pInfo->GetImportSections(&nImportSections) : GetImportSections(&nImportSections); + ReadyToRunLoadedImage * pNativeImage = (pInfo != NULL) ? pInfo->GetImage() : GetReadyToRunImage(); - return FixupDelayListAux(pFixupList, this, &Module::FixupNativeEntry, pImportSections, nImportSections, GetReadyToRunImage(), mayUsePrecompiledPInvokeMethods); + return FixupDelayListAux(pFixupList, this, &Module::FixupNativeEntry, pImportSections, nImportSections, pNativeImage, mayUsePrecompiledPInvokeMethods, pInfo); } template BOOL Module::FixupDelayListAux(TADDR pFixupList, Ptr pThis, FixupNativeEntryCallback pfnCB, PTR_READYTORUN_IMPORT_SECTION pImportSections, COUNT_T nImportSections, - ReadyToRunLoadedImage * pNativeImage, BOOL mayUsePrecompiledPInvokeMethods) + ReadyToRunLoadedImage * pNativeImage, BOOL mayUsePrecompiledPInvokeMethods, + ReadyToRunInfo * pInfo) { CONTRACTL { @@ -454,7 +456,7 @@ BOOL Module::FixupDelayListAux(TADDR pFixupList, { CONSISTENCY_CHECK(fixupIndex * sizeof(TADDR) < cbData); - if (!(pThis->*pfnCB)(pImportSection, fixupIndex, dac_cast(pData + fixupIndex * sizeof(TADDR)), mayUsePrecompiledPInvokeMethods)) + if (!(pThis->*pfnCB)(pImportSection, fixupIndex, dac_cast(pData + fixupIndex * sizeof(TADDR)), mayUsePrecompiledPInvokeMethods, pInfo)) return FALSE; int delta = reader.ReadEncodedU32(); diff --git a/src/coreclr/vm/codeman.cpp b/src/coreclr/vm/codeman.cpp index dcfdcfcf0d78e0..5f3c6a713c661e 100644 --- a/src/coreclr/vm/codeman.cpp +++ b/src/coreclr/vm/codeman.cpp @@ -5979,14 +5979,14 @@ void ExecutionManager::AddCodeRange(TADDR pStartRange, #ifdef TARGET_WASM TADDR ExecutionManager::AddVirtualIPRange(UINT32 numVirtualIPs, IJitManager* pJit, - PTR_Module pModule) + PTR_ReadyToRunInfo pR2RInfo) { CONTRACTL { THROWS; GC_NOTRIGGER; PRECONDITION(numVirtualIPs > 0); PRECONDITION(CheckPointer(pJit)); - PRECONDITION(CheckPointer(pModule)); + PRECONDITION(CheckPointer(pR2RInfo)); } CONTRACTL_END; // Check for odd number of virtual IPs. We require an even number of virtual IPs to ensure that the encoded virtual IP @@ -6019,7 +6019,8 @@ TADDR ExecutionManager::AddVirtualIPRange(UINT32 numVirtualIPs, Range(startVIP, endVIP), pJit, RangeSection::RANGE_SECTION_VIRTUALIP, - pModule); + pR2RInfo->GetModule(), + pR2RInfo); VirtualIPRangeSection* pOldRangeSection = nullptr; do @@ -6049,17 +6050,17 @@ VirtualIPRangeSection* ExecutionManager::FindVirtualIPRangeSection(TADDR virtual void ExecutionManager::AddFunctionTableIndexRange(DWORD minFunctionTableIndex, DWORD numRuntimeFunctions, - PTR_Module pModule) + PTR_ReadyToRunInfo pR2RInfo) { CONTRACTL { THROWS; GC_NOTRIGGER; PRECONDITION(numRuntimeFunctions > 0); - PRECONDITION(CheckPointer(pModule)); + PRECONDITION(CheckPointer(pR2RInfo)); } CONTRACTL_END; FunctionTableIndexRangeSection* pNewRange = new FunctionTableIndexRangeSection( - minFunctionTableIndex, numRuntimeFunctions, pModule); + minFunctionTableIndex, numRuntimeFunctions, pR2RInfo); FunctionTableIndexRangeSection* pOldRangeSection = nullptr; do @@ -6096,8 +6097,7 @@ BOOL ExecutionManager::IsFuncletFunctionIndex(DWORD functionIndex) return FALSE; } - Module* pModule = pSection->pR2RModule; - ReadyToRunInfo* pR2RInfo = pModule->GetReadyToRunInfo(); + ReadyToRunInfo* pR2RInfo = pSection->pR2RInfo; DWORD localIndex = functionIndex - pSection->minFunctionTableIndex; PTR_RUNTIME_FUNCTION pRuntimeFunction = pR2RInfo->GetRuntimeFunctions() + localIndex; @@ -6114,8 +6114,7 @@ TADDR ExecutionManager::GetWasmVirtualIPFromFunctionTableIndex(DWORD functionInd return 0; } - Module* pModule = pSection->pR2RModule; - ReadyToRunInfo* pR2RInfo = pModule->GetReadyToRunInfo(); + ReadyToRunInfo* pR2RInfo = pSection->pR2RInfo; DWORD localIndex = functionIndex - pSection->minFunctionTableIndex; do @@ -6155,8 +6154,7 @@ TADDR ExecutionManager::GetWasmFunctionTableIndexFromVirtualIP(TADDR virtualIP) return 0; } - Module* pModule = pSection->rangeSection._pR2RModule; - ReadyToRunInfo* pR2RInfo = pModule->GetReadyToRunInfo(); + ReadyToRunInfo* pR2RInfo = pSection->rangeSection._pR2RInfo; DWORD runtimeFunctionCount = pR2RInfo->GetRuntimeFunctionCount(); if (runtimeFunctionCount == 0) { @@ -6939,7 +6937,7 @@ PTR_ReadyToRunInfo ReadyToRunJitManager::JitTokenToReadyToRunInfo(const METHODTO SUPPORTS_DAC; } CONTRACTL_END; - return MethodToken.m_pRangeSection->_pR2RModule->GetReadyToRunInfo(); + return MethodToken.m_pRangeSection->_pR2RInfo; } UINT32 ReadyToRunJitManager::JitTokenToGCInfoVersion(const METHODTOKEN& MethodToken) diff --git a/src/coreclr/vm/codeman.h b/src/coreclr/vm/codeman.h index 6e67a78c584d8b..2b58faec2e12f5 100644 --- a/src/coreclr/vm/codeman.h +++ b/src/coreclr/vm/codeman.h @@ -786,6 +786,7 @@ struct RangeSection _flags(flags), _pjit(pJit), _pR2RModule(pR2RModule), + _pR2RInfo(pR2RModule->GetReadyToRunInfo()), _pHeapList(dac_cast((TADDR)0)), _pRangeList(dac_cast((TADDR)0)) #if defined(TARGET_AMD64) @@ -829,6 +830,12 @@ struct RangeSection const RangeSectionFlags _flags; const PTR_IJitManager _pjit; const PTR_Module _pR2RModule; +#ifdef FEATURE_READYTORUN + // The specific R2R image owning this range. Equals _pR2RModule->GetReadyToRunInfo(), except for a + // wasm virtual-IP range of a supplemental (lazily attached) image, where it names that image + // rather than the module's primary info. + PTR_ReadyToRunInfo _pR2RInfo = dac_cast((TADDR)0); +#endif const PTR_HeapList _pHeapList; const PTR_CodeRangeMapRangeList _pRangeList; @@ -2417,8 +2424,11 @@ struct cdac_data #ifdef TARGET_WASM struct VirtualIPRangeSection { - VirtualIPRangeSection(Range range, IJitManager* pJit, RangeSection::RangeSectionFlags flags, PTR_Module pR2RModule) : rangeSection(range, pJit, flags, pR2RModule), pNext(nullptr) + VirtualIPRangeSection(Range range, IJitManager* pJit, RangeSection::RangeSectionFlags flags, PTR_Module pR2RModule, PTR_ReadyToRunInfo pR2RInfo) : rangeSection(range, pJit, flags, pR2RModule), pNext(nullptr) { + // The embedded RangeSection defaulted _pR2RInfo to the module's primary info; override it with + // the specific image that registered this range (which may be a supplemental lazy image). + rangeSection._pR2RInfo = pR2RInfo; } RangeSection rangeSection; // Synthetic RangeSection for compatibility with existing APIs @@ -2434,14 +2444,14 @@ struct VirtualIPRangeSection struct FunctionTableIndexRangeSection { - FunctionTableIndexRangeSection(DWORD minIndex, DWORD count, PTR_Module pModule) - : minFunctionTableIndex(minIndex), numRuntimeFunctions(count), pR2RModule(pModule), pNext(nullptr) + FunctionTableIndexRangeSection(DWORD minIndex, DWORD count, PTR_ReadyToRunInfo pR2RInfo) + : minFunctionTableIndex(minIndex), numRuntimeFunctions(count), pR2RInfo(pR2RInfo), pNext(nullptr) { } DWORD minFunctionTableIndex; // Start of the function table index range DWORD numRuntimeFunctions; // Number of RUNTIME_FUNCTION entries - PTR_Module pR2RModule; // Module owning this range + PTR_ReadyToRunInfo pR2RInfo; // R2R image owning this range FunctionTableIndexRangeSection* pNext; // Next entry in the linked list }; #endif // TARGET_WASM @@ -2589,9 +2599,7 @@ class ExecutionManager // Returns the start virtual IP assigned to this module. static TADDR AddVirtualIPRange(UINT32 numVirtualIPs, IJitManager* pJit, - PTR_Module pModule); - - // Find the VirtualIPRangeSection for a given virtual IP. + PTR_ReadyToRunInfo pR2RInfo); static VirtualIPRangeSection* FindVirtualIPRangeSection(TADDR virtualIP); // Returns true if the given PCODE is a virtual IP encoding (both low and high bits set). @@ -2607,7 +2615,7 @@ class ExecutionManager // Register a function table index range for a WASM R2R module. static void AddFunctionTableIndexRange(DWORD minFunctionTableIndex, DWORD numRuntimeFunctions, - PTR_Module pModule); + PTR_ReadyToRunInfo pR2RInfo); // Find the FunctionTableIndexRangeSection for a given function table index. static FunctionTableIndexRangeSection* FindFunctionTableIndexRangeSection(DWORD functionIndex); @@ -2813,7 +2821,7 @@ struct cdac_data { static constexpr size_t MinFunctionTableIndex = offsetof(FunctionTableIndexRangeSection, minFunctionTableIndex); static constexpr size_t NumRuntimeFunctions = offsetof(FunctionTableIndexRangeSection, numRuntimeFunctions); - static constexpr size_t R2RModule = offsetof(FunctionTableIndexRangeSection, pR2RModule); + static constexpr size_t R2RInfo = offsetof(FunctionTableIndexRangeSection, pR2RInfo); static constexpr size_t Next = offsetof(FunctionTableIndexRangeSection, pNext); }; #endif // TARGET_WASM diff --git a/src/coreclr/vm/codeman.inl b/src/coreclr/vm/codeman.inl index 9f70457a42a3e3..625a8d8d4c6f4a 100644 --- a/src/coreclr/vm/codeman.inl +++ b/src/coreclr/vm/codeman.inl @@ -13,7 +13,7 @@ inline TADDR IJitManager::JitTokenToModuleRVABase(const METHODTOKEN& MethodToken { #ifdef TARGET_WASM if (MethodToken.m_pRangeSection->_flags & RangeSection::RANGE_SECTION_VIRTUALIP) - return dac_cast(MethodToken.m_pRangeSection->_pR2RModule->GetReadyToRunInfo()->GetImage()->GetBase()); + return dac_cast(MethodToken.m_pRangeSection->_pR2RInfo->GetImage()->GetBase()); #endif // For non-wasm, the rva base is always the same as the range base. return MethodToken.m_pRangeSection->_range.RangeStart(); diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 25c22dc3a928fc..da178b535bac3d 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -977,7 +977,7 @@ CDAC_TYPE_BEGIN(FunctionTableIndexRangeSection) CDAC_TYPE_INDETERMINATE(FunctionTableIndexRangeSection) CDAC_TYPE_FIELD(FunctionTableIndexRangeSection, T_UINT32, MinFunctionTableIndex, cdac_data::MinFunctionTableIndex) CDAC_TYPE_FIELD(FunctionTableIndexRangeSection, T_UINT32, NumRuntimeFunctions, cdac_data::NumRuntimeFunctions) -CDAC_TYPE_FIELD(FunctionTableIndexRangeSection, T_POINTER, R2RModule, cdac_data::R2RModule) +CDAC_TYPE_FIELD(FunctionTableIndexRangeSection, T_POINTER, R2RInfo, cdac_data::R2RInfo) CDAC_TYPE_FIELD(FunctionTableIndexRangeSection, T_POINTER, Next, cdac_data::Next) CDAC_TYPE_END(FunctionTableIndexRangeSection) #endif // TARGET_WASM diff --git a/src/coreclr/vm/frames.cpp b/src/coreclr/vm/frames.cpp index 5af329369f7ade..f1df384b729326 100644 --- a/src/coreclr/vm/frames.cpp +++ b/src/coreclr/vm/frames.cpp @@ -863,11 +863,27 @@ static PTR_BYTE FindGCRefMap(PTR_Module pZapModule, TADDR ptr) { LIMITED_METHOD_DAC_CONTRACT; - ReadyToRunLoadedImage *pNativeImage = pZapModule->GetReadyToRunImage(); + // The import cell may live in a lazily-attached supplemental R2R image rather than the module's + // primary image; resolve the owning image by address so the RVA and GC ref map come from it. + PTR_ReadyToRunInfo pInfo = pZapModule->GetReadyToRunInfo(); + for (PTR_ReadyToRunInfo pSupplemental = pZapModule->GetSupplementalReadyToRunInfos(); + pSupplemental != NULL; + pSupplemental = pSupplemental->GetNextSupplemental()) + { + ReadyToRunLoadedImage *pImage = pSupplemental->GetImage(); + TADDR base = pImage->GetBase(); + if (ptr >= base && ptr < base + pImage->GetVirtualSize()) + { + pInfo = pSupplemental; + break; + } + } + + ReadyToRunLoadedImage *pNativeImage = pInfo->GetImage(); RVA rva = pNativeImage->GetDataRva(ptr); - PTR_READYTORUN_IMPORT_SECTION pImportSection = pZapModule->GetImportSectionForRVA(rva); + PTR_READYTORUN_IMPORT_SECTION pImportSection = pInfo->GetImportSectionForRVA(rva); if (pImportSection == NULL) return NULL; diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 6ec53518659e9f..4e32c673a82657 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -14400,11 +14400,16 @@ bool IsInstructionSetSupported(CORJIT_FLAGS jitFlags, ReadyToRunInstructionSet r BOOL LoadDynamicInfoEntry(Module *currentModule, RVA fixupRva, SIZE_T *entry, - BOOL mayUsePrecompiledPInvokeMethods) + BOOL mayUsePrecompiledPInvokeMethods, + ReadyToRunInfo *pSignatureSource) { STANDARD_VM_CONTRACT; - PCCOR_SIGNATURE pBlob = currentModule->GetNativeFixupBlobData(fixupRva); + // A supplemental (lazily-attached) R2R image stores its fixup signatures in its own image, not in + // currentModule's primary R2R image, so resolve the blob against it when provided. + PCCOR_SIGNATURE pBlob = (pSignatureSource != NULL) + ? (PCCOR_SIGNATURE)pSignatureSource->GetImage()->GetRvaData(fixupRva) + : currentModule->GetNativeFixupBlobData(fixupRva); BYTE kind = *pBlob++; @@ -14412,7 +14417,8 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, if (kind & READYTORUN_FIXUP_ModuleOverride) { - pInfoModule = currentModule->GetModuleFromIndex(CorSigUncompressData(pBlob)); + // Resolve the module index against the supplemental image's manifest when this fixup comes from one. + pInfoModule = currentModule->GetModuleFromIndex(CorSigUncompressData(pBlob), pSignatureSource); kind &= ~READYTORUN_FIXUP_ModuleOverride; } @@ -14425,7 +14431,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, case READYTORUN_FIXUP_TypeHandle: case READYTORUN_FIXUP_TypeDictionary: { - TypeHandle th = ZapSig::DecodeType(currentModule, pInfoModule, pBlob); + TypeHandle th = ZapSig::DecodeType(currentModule, pInfoModule, pBlob, CLASS_LOADED, NULL, pSignatureSource); if (!th.IsTypeDesc()) { @@ -14443,7 +14449,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, case READYTORUN_FIXUP_MethodHandle: case READYTORUN_FIXUP_MethodDictionary: { - MethodDesc * pMD = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob); + MethodDesc * pMD = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob, NULL, pSignatureSource); if (currentModule->IsReadyToRun()) { @@ -14462,7 +14468,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, // found for it may belong to a canonical instantiation of that base type, so walk the parent chain of // the (exact) type from the token to recover the exact declaring type. TypeHandle thOwner; - MethodDesc * pMethod = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob, &thOwner); + MethodDesc * pMethod = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob, &thOwner, pSignatureSource); MethodTable * pOwnerMT = thOwner.GetMethodTable(); MethodTable * pDeclaringMT; @@ -14490,7 +14496,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, break; case READYTORUN_FIXUP_FieldHandle: - result = (size_t) ZapSig::DecodeField(currentModule, pInfoModule, pBlob); + result = (size_t) ZapSig::DecodeField(currentModule, pInfoModule, pBlob, NULL, pSignatureSource); break; case READYTORUN_FIXUP_StringHandle: @@ -14555,7 +14561,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, case READYTORUN_FIXUP_MethodEntry: { - pMD = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob); + pMD = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob, NULL, pSignatureSource); if (currentModule->IsReadyToRun()) { @@ -14571,7 +14577,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, #ifdef HAS_PINVOKE_IMPORT_PRECODE case READYTORUN_FIXUP_IndirectPInvokeTarget: { - MethodDesc *pMethod = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob); + MethodDesc *pMethod = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob, NULL, pSignatureSource); _ASSERTE(pMethod->IsPInvoke()); PInvokeMethodDesc *pMD = (PInvokeMethodDesc*)pMethod; @@ -14584,7 +14590,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, { if (mayUsePrecompiledPInvokeMethods) { - MethodDesc *pMethod = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob); + MethodDesc *pMethod = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob, NULL, pSignatureSource); _ASSERTE(pMethod->IsPInvoke()); PInvoke::ResolvePInvokeTarget((PInvokeMethodDesc*)pMethod); @@ -14599,7 +14605,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, case READYTORUN_FIXUP_FieldAddress: { - FieldDesc *pField = ZapSig::DecodeField(currentModule, pInfoModule, pBlob); + FieldDesc *pField = ZapSig::DecodeField(currentModule, pInfoModule, pBlob, NULL, pSignatureSource); pField->GetEnclosingMethodTable()->CheckRestore(); @@ -14704,7 +14710,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, case READYTORUN_FIXUP_FieldOffset: { - FieldDesc * pFD = ZapSig::DecodeField(currentModule, pInfoModule, pBlob); + FieldDesc * pFD = ZapSig::DecodeField(currentModule, pInfoModule, pBlob, NULL, pSignatureSource); _ASSERTE(!pFD->IsStatic()); _ASSERTE(!pFD->IsFieldOfValueType()); @@ -14718,7 +14724,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, case READYTORUN_FIXUP_FieldBaseOffset: { - TypeHandle th = ZapSig::DecodeType(currentModule, pInfoModule, pBlob); + TypeHandle th = ZapSig::DecodeType(currentModule, pInfoModule, pBlob, CLASS_LOADED, NULL, pSignatureSource); MethodTable * pMT = th.AsMethodTable(); _ASSERTE(!pMT->IsValueType()); @@ -14733,7 +14739,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, case READYTORUN_FIXUP_Check_TypeLayout: case READYTORUN_FIXUP_Verify_TypeLayout: { - TypeHandle th = ZapSig::DecodeType(currentModule, pInfoModule, pBlob); + TypeHandle th = ZapSig::DecodeType(currentModule, pInfoModule, pBlob, CLASS_LOADED, NULL, pSignatureSource); MethodTable * pMT = th.AsMethodTable(); _ASSERTE(pMT->IsValueType()); @@ -14838,7 +14844,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, { DWORD dwExpectedOffset = CorSigUncompressData(pBlob); - FieldDesc * pFD = ZapSig::DecodeField(currentModule, pInfoModule, pBlob); + FieldDesc * pFD = ZapSig::DecodeField(currentModule, pInfoModule, pBlob, NULL, pSignatureSource); _ASSERTE(!pFD->IsStatic()); DWORD dwOffset = pFD->GetOffset(); @@ -14856,7 +14862,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, { DWORD baseOffset = CorSigUncompressData(pBlob); DWORD fieldOffset = CorSigUncompressData(pBlob); - FieldDesc* pField = ZapSig::DecodeField(currentModule, pInfoModule, pBlob); + FieldDesc* pField = ZapSig::DecodeField(currentModule, pInfoModule, pBlob, NULL, pSignatureSource); MethodTable *pEnclosingMT = pField->GetApproxEnclosingMethodTable(); pEnclosingMT->CheckRestore(); DWORD actualFieldOffset = pField->GetOffset(); @@ -14911,15 +14917,15 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, ReadyToRunVirtualFunctionOverrideFlags flags = (ReadyToRunVirtualFunctionOverrideFlags)CorSigUncompressData(updatedSignature); SigTypeContext typeContext; // empty context is OK: encoding should not contain type variables. - ZapSig::Context zapSigContext(pInfoModule, (void *)currentModule, ZapSig::NormalTokens); + ZapSig::Context zapSigContext(pInfoModule, (void *)currentModule, ZapSig::NormalTokens, pSignatureSource); MethodDesc *pDeclMethod = ZapSig::DecodeMethod(pInfoModule, updatedSignature, &typeContext, &zapSigContext, NULL, NULL, NULL, &updatedSignature, TRUE); - TypeHandle thImpl = ZapSig::DecodeType(currentModule, pInfoModule, updatedSignature, CLASS_LOADED, &updatedSignature); + TypeHandle thImpl = ZapSig::DecodeType(currentModule, pInfoModule, updatedSignature, CLASS_LOADED, &updatedSignature, pSignatureSource); MethodDesc *pImplMethodCompiler = NULL; if ((flags & READYTORUN_VIRTUAL_OVERRIDE_VirtualFunctionOverridden) != 0) { - pImplMethodCompiler = ZapSig::DecodeMethod(currentModule, pInfoModule, updatedSignature); + pImplMethodCompiler = ZapSig::DecodeMethod(currentModule, pInfoModule, updatedSignature, NULL, pSignatureSource); } MethodDesc *pImplMethodRuntime; @@ -15085,7 +15091,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, { EX_TRY { - types.Append(ZapSig::DecodeType(currentModule, pInfoModule, pBlob, CLASS_LOAD_APPROXPARENTS, &pBlob)); + types.Append(ZapSig::DecodeType(currentModule, pInfoModule, pBlob, CLASS_LOAD_APPROXPARENTS, &pBlob, pSignatureSource)); } EX_CATCH { @@ -15096,7 +15102,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, } else { - types.Append(ZapSig::DecodeType(currentModule, pInfoModule, pBlob, CLASS_LOAD_APPROXPARENTS, &pBlob)); + types.Append(ZapSig::DecodeType(currentModule, pInfoModule, pBlob, CLASS_LOAD_APPROXPARENTS, &pBlob, pSignatureSource)); } } @@ -15108,7 +15114,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, { EX_TRY { - pMDCompare = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob); + pMDCompare = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob, NULL, pSignatureSource); } EX_CATCH { @@ -15118,7 +15124,7 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, } else { - pMDCompare = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob); + pMDCompare = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob, NULL, pSignatureSource); } } diff --git a/src/coreclr/vm/jitinterface.h b/src/coreclr/vm/jitinterface.h index d0a26f4adaa838..e12959ef75dcae 100644 --- a/src/coreclr/vm/jitinterface.h +++ b/src/coreclr/vm/jitinterface.h @@ -84,10 +84,16 @@ void getMethodInfoILMethodHeaderHelper( ); +class ReadyToRunInfo; + +// When pSignatureSource is non-NULL, the fixup signature blob is read from that R2R image (a +// lazily-attached supplemental image) instead of currentModule's primary R2R image, while +// currentModule still provides the type-system context. Used by Module::RunSupplementalEagerFixups. BOOL LoadDynamicInfoEntry(Module *currentModule, RVA fixupRva, SIZE_T *entry, - BOOL mayUsePrecompiledPInvokeMethods = TRUE); + BOOL mayUsePrecompiledPInvokeMethods = TRUE, + ReadyToRunInfo *pSignatureSource = NULL); // These must be implemented in assembly and generate a TransitionBlock then calling JIT_PatchpointWorkerWithPolicy in order to actually be used. EXTERN_C FCDECL2(PCODE, JIT_Patchpoint, int* counter, int ilOffset); diff --git a/src/coreclr/vm/nativeimage.cpp b/src/coreclr/vm/nativeimage.cpp index 5c197a2186bbe0..384e75265439c2 100644 --- a/src/coreclr/vm/nativeimage.cpp +++ b/src/coreclr/vm/nativeimage.cpp @@ -9,7 +9,9 @@ #include "common.h" #include "nativeimage.h" #include "hostinformation.h" - +#ifdef TARGET_WASM +#include "webcildecoder.h" +#endif // -------------------------------------------------------------------------------- // Headers // -------------------------------------------------------------------------------- @@ -320,6 +322,120 @@ NativeImage *NativeImage::Open( } #endif +#if defined(TARGET_WASM) && !defined(DACCESS_COMPILE) + +// IMAGE_REL_BASED_PTR is the architecture-specific virtual-address reloc (see PEImageLayout::ApplyBaseRelocations). +#ifdef TARGET_64BIT +#define IMAGE_REL_BASED_PTR IMAGE_REL_BASED_DIR64 +#else +#define IMAGE_REL_BASED_PTR IMAGE_REL_BASED_HIGHLOW +#endif + +// A lazily-attached supplemental R2R image is a host-allocated, memory-resident webcil buffer opened via a +// plain ReadyToRunLoadedImage view -- it never passes through PEImageLayout::ApplyBaseRelocations the way the +// eager (startup-loaded) webcil R2R images do. Its wasm function-table indices (and the min-function-table-index +// stored after the RUNTIME_FUNCTION sentinel) are baked base-0 by crossgen and are meant to be relocated by the +// runtime table base where the host actually placed the module's functions (grown into the shared indirect table +// at load time, written into the webcil header's TableBase by getWebcilPayload). Without this relocation the VM +// computes wrong entry-point indices and the first interp->R2R call traps with "null function or function +// signature mismatch". Apply the same relocations here that ApplyBaseRelocations applies to eager webcil images: +// IMAGE_REL_BASED_PTR (+= load delta; preferred base is 0 for webcil so delta == imageBase) and the additive +// IMAGE_REL_BASED_WASM32/64_TABLE (+= tableBase). The buffer is host-owned and writable, so no page protection +// dance is needed, and it is opened exactly once so a single application is correct. +static void ApplyLazySupplementalWebcilRelocations(TADDR imageBase, WebcilDecoder &decoder) +{ + STANDARD_VM_CONTRACT; + + if (!decoder.HasDirectoryEntry(IMAGE_DIRECTORY_ENTRY_BASERELOC)) + return; + + const SSIZE_T delta = (SSIZE_T)imageBase; // GetPreferredBase() == NULL for webcil + const SSIZE_T tableBaseDelta = decoder.GetTableBaseOffset(); + + COUNT_T dirSize = 0; + TADDR dir = decoder.GetDirectoryEntryData(IMAGE_DIRECTORY_ENTRY_BASERELOC, &dirSize); + + COUNT_T dirPos = 0; + // WASM pads each reloc block to a 16-byte boundary, so validate the header is fully readable and stop on a + // zero-sized (padding) block, mirroring PEImageLayout::ApplyBaseRelocations. + while (dirPos + sizeof(IMAGE_BASE_RELOCATION) <= dirSize) + { + PIMAGE_BASE_RELOCATION r = (PIMAGE_BASE_RELOCATION)(dir + dirPos); + COUNT_T fixupsSize = VAL32(r->SizeOfBlock); + if (fixupsSize == 0) + break; + + USHORT *fixups = (USHORT *)(r + 1); + COUNT_T fixupsCount = (fixupsSize - sizeof(IMAGE_BASE_RELOCATION)) / 2; + BYTE *pageAddress = (BYTE *)imageBase + VAL32(r->VirtualAddress); + + for (COUNT_T i = 0; i < fixupsCount; i++) + { + USHORT fixup = VAL16(fixups[i]); + BYTE *address = pageAddress + (fixup & 0xfff); + switch (fixup >> 12) + { + case IMAGE_REL_BASED_PTR: + *(TADDR *)address += delta; + break; + case IMAGE_REL_BASED_WASM32_TABLE: + *(uint32_t *)address += (uint32_t)tableBaseDelta; + break; + case IMAGE_REL_BASED_WASM64_TABLE: + *(uint64_t *)address += (uint64_t)tableBaseDelta; + break; + case IMAGE_REL_BASED_ABSOLUTE: + break; + default: + break; + } + } + dirPos += fixupsSize; + } +} +NativeImage *NativeImage::OpenFromMemory( + TADDR imageBase, + uint32_t imageSize, + LPCUTF8 nativeImageFileName, + AssemblyBinder *pAssemblyBinder, + LoaderAllocator *pLoaderAllocator, + AllocMemTracker *pamTracker) +{ + STANDARD_VM_CONTRACT; + + WebcilDecoder decoder; + decoder.Init((void *)imageBase, (COUNT_T)imageSize); + if (!decoder.HasReadyToRunHeader()) + { + COMPlusThrowHR(COR_E_BADIMAGEFORMAT); + } + + READYTORUN_HEADER *pHeader = decoder.GetReadyToRunHeader(); + if (pHeader->Signature != READYTORUN_SIGNATURE) + { + COMPlusThrowHR(COR_E_BADIMAGEFORMAT); + } + if (pHeader->MajorVersion < MINIMUM_READYTORUN_MAJOR_VERSION || pHeader->MajorVersion > READYTORUN_MAJOR_VERSION) + { + COMPlusThrowHR(COR_E_BADIMAGEFORMAT); + } + + // Relocate the memory-resident buffer to the runtime table base before any structure reads its baked + // (base-0) function-table indices. Must run before the ReadyToRunInfo ctor reads m_minFunctionTableIndex. + ApplyLazySupplementalWebcilRelocations(imageBase, decoder); + + // The payload buffer has process lifetime (allocated by the host loader), so the image layout is + // a plain view over it with no cleanup callback. + NewHolder loadedImageHolder = new ReadyToRunLoadedImage(imageBase, imageSize); + NewHolder image = new NativeImage(pAssemblyBinder, loadedImageHolder.Extract(), nativeImageFileName); + image->Initialize(pHeader, pLoaderAllocator, pamTracker); + + // Supplemental images are not registered in the AppDomain native-image-by-name map; they are owned + // by the module they attach to (see ReadyToRunInfo::AttachSupplemental). + return image.Extract(); +} +#endif // TARGET_WASM && !DACCESS_COMPILE + #ifndef DACCESS_COMPILE Assembly *NativeImage::LoadManifestAssembly(uint32_t rowid, Assembly *pParentAssembly) { diff --git a/src/coreclr/vm/nativeimage.h b/src/coreclr/vm/nativeimage.h index 56a9abc7baa9b2..fff69f201ed487 100644 --- a/src/coreclr/vm/nativeimage.h +++ b/src/coreclr/vm/nativeimage.h @@ -113,6 +113,18 @@ class NativeImage LoaderAllocator *pLoaderAllocator, bool isPlatformNative); +#ifdef TARGET_WASM + // Build a NativeImage from an already-resident webcil composite payload (base/size), rather than + // resolving it by path. Used to attach a lazily-downloaded R2R code supplement in the browser. + static NativeImage *OpenFromMemory( + TADDR imageBase, + uint32_t imageSize, + LPCUTF8 nativeImageFileName, + AssemblyBinder *pAssemblyBinder, + LoaderAllocator *pLoaderAllocator, + AllocMemTracker *pamTracker); +#endif // TARGET_WASM + Crst *EagerFixupsLock() { return &m_eagerFixupsLock; } bool EagerFixupsHaveRun() const { return m_eagerFixupsHaveRun; } void SetEagerFixupsHaveRun() { m_eagerFixupsHaveRun = true; } diff --git a/src/coreclr/vm/prestub.cpp b/src/coreclr/vm/prestub.cpp index 0e265a8a5ce783..904e8c961702ba 100644 --- a/src/coreclr/vm/prestub.cpp +++ b/src/coreclr/vm/prestub.cpp @@ -487,6 +487,38 @@ PCODE MethodDesc::GetPrecompiledR2RCode(PrepareCodeConfig* pConfig) pCode = pAlreadyExaminedInfos[0]->GetEntryPoint(this, pConfig, TRUE /* fFixups */); } + // Lazily-attached supplemental R2R images for this module (native code downloaded after load + // whose metadata/IL live in the module's primary image). The list is empty for all modules + // today, so this walk is a no-op until such an image is attached. + // + // UnmanagedCallersOnly carve-out: a reverse-P/Invoke thunk (the stable Call_ address handed to + // native/JS by GetUnmanagedCallersOnlyThunk) dispatches through ExecuteInterpretedMethodFromUnmanaged, + // which requires interpreter byte code. Letting a UCO method pick up native code from a supplement + // attached after that thunk was handed out would break that path, so supplemental images never provide + // code for UCO methods (they stay interpreted). The primary image probe above is unaffected. + // + // Generic/async-variant/unboxing carve-out: those methods are looked up in the R2R instance-method + // table, whose signature matching (SigMatchesMethodDesc) decodes owner-type signatures that reference + // native assembly-ref indices. That decode resolves indices through m_pModule's PRIMARY ReadyToRunInfo + // (GetNativeAssemblyImport), which for a lazily-attached supplement is the eager image, not this + // supplement's manifest -- and for an IL-only eager image there is no native manifest at all, so the + // resolution faults. Until signature matching can be made supplemental-manifest-aware, these methods + // stay interpreted when their code lives only in a supplement (the methoddef-table path used by + // non-generic methods needs no such decode and is unaffected). + if (pCode == (PCODE)NULL + && !HasUnmanagedCallersOnlyAttribute() + && !HasClassOrMethodInstantiation() + && !IsAsyncVariantMethod() + && !IsUnboxingStub()) + { + for (ReadyToRunInfo* pSupplemental = pModule->GetSupplementalReadyToRunInfos(); + pSupplemental != NULL && pCode == (PCODE)NULL; + pSupplemental = pSupplemental->GetNextSupplemental()) + { + pCode = pSupplemental->GetEntryPoint(this, pConfig, TRUE /* fFixups */); + } + } + // Generics may be located in several places if (pCode == (PCODE)NULL && HasClassOrMethodInstantiation()) { @@ -2794,6 +2826,27 @@ static PCODE PatchNonVirtualExternalMethod(MethodDesc * pMD, PCODE pCode, PTR_RE return pCode; } +#ifdef FEATURE_READYTORUN +// A delay-load fixup cell lives in the R2R image whose code references it. For a lazily-attached +// supplemental (wasm) R2R image that image is NOT the module's primary image, so resolve the cell's +// owning image/info by address rather than assuming the primary. Falls back to the primary info. +static ReadyToRunInfo * GetReadyToRunInfoForImportCell(Module * pModule, TADDR cell) +{ + STANDARD_VM_CONTRACT; + + for (ReadyToRunInfo * pSupplemental = pModule->GetSupplementalReadyToRunInfos(); + pSupplemental != NULL; + pSupplemental = pSupplemental->GetNextSupplemental()) + { + ReadyToRunLoadedImage * pImage = pSupplemental->GetImage(); + TADDR base = pImage->GetBase(); + if (cell >= base && cell < base + pImage->GetVirtualSize()) + return pSupplemental; + } + return pModule->GetReadyToRunInfo(); +} +#endif // FEATURE_READYTORUN + //========================================================================================== // In NGen images calls to external methods start out pointing to jump thunks. // These jump thunks initially point to the assembly code _ExternalMethodFixupStub @@ -2883,7 +2936,8 @@ EXTERN_C PCODE STDCALL ExternalMethodFixupWorker( { GCX_PREEMP_THREAD_EXISTS(CURRENT_THREAD); - ReadyToRunLoadedImage *pNativeImage = pModule->GetReadyToRunImage(); + ReadyToRunInfo *pReadyToRunInfo = GetReadyToRunInfoForImportCell(pModule, pIndirection); + ReadyToRunLoadedImage *pNativeImage = pReadyToRunInfo->GetImage(); RVA rva = pNativeImage->GetDataRva(pIndirection); @@ -2893,13 +2947,13 @@ EXTERN_C PCODE STDCALL ExternalMethodFixupWorker( // On some platforms (everywhere except wasm) we can get the section index from the callsite, // so we don't have to search for it. pImportSection = pModule->GetImportSectionFromIndex(sectionIndex); - _ASSERTE(pImportSection == pModule->GetImportSectionForRVA(rva)); + _ASSERTE(pImportSection == pReadyToRunInfo->GetImportSectionForRVA(rva)); } else { // On some platforms (currently only wasm) we would need to bloat the R2R binary a bit to store // the section index, so we search for it instead. - pImportSection = pModule->GetImportSectionForRVA(rva); + pImportSection = pReadyToRunInfo->GetImportSectionForRVA(rva); } _ASSERTE(pImportSection != NULL); @@ -2918,7 +2972,8 @@ EXTERN_C PCODE STDCALL ExternalMethodFixupWorker( if (kind & READYTORUN_FIXUP_ModuleOverride) { DWORD moduleIndex = CorSigUncompressData(pBlob); - pInfoModule = pModule->GetModuleFromIndex(moduleIndex); + pInfoModule = pModule->GetModuleFromIndex(moduleIndex, + (pReadyToRunInfo != pModule->GetReadyToRunInfo()) ? pReadyToRunInfo : NULL); kind &= ~READYTORUN_FIXUP_ModuleOverride; } @@ -3578,7 +3633,8 @@ PCODE DynamicHelperFixup(TransitionBlock * pTransitionBlock, TADDR * pCell, DWOR { STANDARD_VM_CONTRACT; - ReadyToRunLoadedImage *pNativeImage = pModule->GetReadyToRunImage(); + ReadyToRunInfo *pReadyToRunInfo = GetReadyToRunInfoForImportCell(pModule, (TADDR)pCell); + ReadyToRunLoadedImage *pNativeImage = pReadyToRunInfo->GetImage(); RVA rva = pNativeImage->GetDataRva((TADDR)pCell); @@ -3593,10 +3649,10 @@ PCODE DynamicHelperFixup(TransitionBlock * pTransitionBlock, TADDR * pCell, DWOR { // On some platforms (currently only wasm) we would need to bloat the R2R binary a bit to store // the section index, so we search for it instead. - pImportSection = pModule->GetImportSectionForRVA(rva); + pImportSection = pReadyToRunInfo->GetImportSectionForRVA(rva); } - _ASSERTE(pImportSection == pModule->GetImportSectionForRVA(rva)); + _ASSERTE(pImportSection == pReadyToRunInfo->GetImportSectionForRVA(rva)); _ASSERTE(pImportSection->EntrySize == sizeof(TADDR)); @@ -3613,7 +3669,8 @@ PCODE DynamicHelperFixup(TransitionBlock * pTransitionBlock, TADDR * pCell, DWOR if (kind & READYTORUN_FIXUP_ModuleOverride) { DWORD moduleIndex = CorSigUncompressData(pBlob); - pInfoModule = pModule->GetModuleFromIndex(moduleIndex); + pInfoModule = pModule->GetModuleFromIndex(moduleIndex, + (pReadyToRunInfo != pModule->GetReadyToRunInfo()) ? pReadyToRunInfo : NULL); kind = (ReadyToRunFixupKind)(kind & ~READYTORUN_FIXUP_ModuleOverride); } diff --git a/src/coreclr/vm/readytoruninfo.cpp b/src/coreclr/vm/readytoruninfo.cpp index f4646c8b0271c6..d6bda5a1dbeac1 100644 --- a/src/coreclr/vm/readytoruninfo.cpp +++ b/src/coreclr/vm/readytoruninfo.cpp @@ -14,6 +14,7 @@ #include "versionresilienthashcode.h" #include "typehashingalgorithms.h" #include "method.hpp" +#include "methoditer.h" #include "wellknownattributes.h" #include "nativeimage.h" #include "dn-stdio.h" @@ -804,7 +805,8 @@ ReadyToRunInfo::ReadyToRunInfo(Module * pModule, LoaderAllocator* pLoaderAllocat m_readyToRunCodeDisabled(FALSE), m_Crst(CrstReadyToRunEntryPointToMethodDescMap), m_pPersistentInlineTrackingMap(NULL), - m_pNextR2RForUnrelatedCode(NULL) + m_pNextR2RForUnrelatedCode(NULL), + m_pNextSupplemental(NULL) { STANDARD_VM_CONTRACT; @@ -1107,7 +1109,7 @@ static bool SigMatchesMethodDesc(MethodDesc* pMD, SigPointer &sig, ModuleBase * { uint32_t updatedModuleIndex; IfFailThrow(sig.GetData(&updatedModuleIndex)); - pModule = pZapSigContext->GetZapSigModule()->GetModuleFromIndex(updatedModuleIndex); + pModule = pZapSigContext->GetZapSigModule()->GetModuleFromIndex(updatedModuleIndex, pZapSigContext->pR2RInfo); } if (methodFlags & ENCODE_METHOD_SIG_OwnerType) @@ -1404,7 +1406,7 @@ PCODE ReadyToRunInfo::GetEntryPoint(MethodDesc * pMD, PrepareCodeConfig* pConfig BOOL mayUsePrecompiledPInvokeMethods = TRUE; mayUsePrecompiledPInvokeMethods = !pConfig->IsForMulticoreJit(); - if (!m_pModule->FixupDelayList(dac_cast(GetImage()->GetBase()) + offset, mayUsePrecompiledPInvokeMethods)) + if (!m_pModule->FixupDelayList(dac_cast(GetImage()->GetBase()) + offset, mayUsePrecompiledPInvokeMethods, this)) { pConfig->SetReadyToRunRejectedPrecompiledCode(); goto done; @@ -2969,12 +2971,11 @@ static void ProcessWasmAsyncResumeInfoFixups( } } -void ReadyToRunInfo::RegisterVirtualIPRange(Module* pModule) +void ReadyToRunInfo::RegisterVirtualIPRange() { CONTRACTL { THROWS; GC_NOTRIGGER; - PRECONDITION(CheckPointer(pModule)); } CONTRACTL_END; if (m_nRuntimeFunctions == 0) @@ -3000,7 +3001,7 @@ void ReadyToRunInfo::RegisterVirtualIPRange(Module* pModule) m_pComposite->SetMinVirtualIP(ExecutionManager::AddVirtualIPRange( totalVirtualIPs, ExecutionManager::GetReadyToRunJitManager(), - pModule)); + this)); IMAGE_DATA_DIRECTORY* pAsyncResumeInfoFixups = m_pComposite->FindSection(ReadyToRunSectionType::WasmAsyncResumeInfo); @@ -3017,11 +3018,83 @@ void ReadyToRunInfo::RegisterVirtualIPRange(Module* pModule) ExecutionManager::AddFunctionTableIndexRange( m_minFunctionTableIndex, m_nRuntimeFunctions, - pModule); + this); } m_minVirtualIP = m_pComposite->GetMinVirtualIP(); } + +ReadyToRunInfo *ReadyToRunInfo::AttachSupplemental(Module *pModule, NativeImage *pLazyImage, AllocMemTracker *pamTracker) +{ + STANDARD_VM_CONTRACT; + _ASSERTE(pModule != NULL); + _ASSERTE(pLazyImage != NULL); + + // The lazy composite must carry a component matching this module's assembly; otherwise it is not a + // supplement for this module and the attach is declined. + if (pLazyImage->GetComponentAssemblyHeader(pModule->GetSimpleName()) == NULL) + return NULL; + + LoaderAllocator *pLoaderAllocator = pModule->GetLoaderAllocator(); + READYTORUN_HEADER *pHeader = pLazyImage->GetReadyToRunInfo()->GetReadyToRunHeader(); + + void *pMemory = pamTracker->Track(pLoaderAllocator->GetHighFrequencyHeap()->AllocMem(S_SIZE_T(sizeof(ReadyToRunInfo)))); + ReadyToRunInfo *pInfo = new (pMemory) ReadyToRunInfo(pModule, pLoaderAllocator, pHeader, pLazyImage, /*pLayout*/ nullptr, pamTracker); + + // Register the image's virtual-IP and function-table ranges so its code is unwindable, run its + // eager fixups (notably InjectStringThunks) so its thunks resolve, then publish it on the module + // so GetPrecompiledR2RCode resolves the complement methods' entrypoints. + pInfo->RegisterVirtualIPRange(); + pModule->RunSupplementalEagerFixups(pInfo); + pModule->AttachSupplementalReadyToRunInfo(pInfo); + + // The caller re-points already-loaded interpreted methods (RebindLoadedInterpretedMethods) only after + // committing the allocation, so a rebind failure cannot release an image already linked on the module. + return pInfo; +} + +// After a supplemental (lazy) image is attached, any method that was already called resolved to the +// interpreter and cached its byte code, so INTOP_CALL no longer consults the portable entrypoint and +// would keep interpreting instead of using the newly available native code. Re-point every already-loaded, +// interpreter-resolved instance of the methods this image provides: poison the cached interpreter code so +// the interpreter dispatches through the portable entrypoint, and reset the entrypoint so its next +// invocation re-runs the prestub -> GetPrecompiledR2RCode -> this image's entrypoint. Methods not yet +// called are left alone (their first call naturally finds the native code); methods this image does not +// provide are never touched (poisoning one with no native code would trap when the prestub falls back to +// the interpreter). UnmanagedCallersOnly methods are excluded to match the carve-out in +// GetPrecompiledR2RCode: their reverse-P/Invoke thunk requires interpreter byte code, so they stay +// interpreted. Generic method instantiations (m_instMethodEntryPoints) are not handled here yet. +void ReadyToRunInfo::RebindLoadedInterpretedMethods() +{ + STANDARD_VM_CONTRACT; + +#if defined(FEATURE_INTERPRETER) && defined(FEATURE_PORTABLE_ENTRYPOINTS) + AppDomain *pAppDomain = AppDomain::GetCurrentDomain(); + uint count = m_methodDefEntryPoints.GetCount(); + for (uint index = 0; index < count; index++) + { + uint offset; + if (!m_methodDefEntryPoints.TryGetAt(index, &offset)) + continue; + + mdMethodDef token = mdtMethodDef | (index + 1); + LoadedMethodDescIterator mdIt(pAppDomain, m_pModule, token); + CollectibleAssemblyHolder pAssembly; + while (mdIt.Next(pAssembly.This())) + { + MethodDesc *pMD = mdIt.Current(); + if (pMD == NULL) + continue; + + if (pMD->GetInterpreterCode() != NULL && !pMD->HasUnmanagedCallersOnlyAttribute()) + { + pMD->PoisonInterpreterCode(); + pMD->ResetPortableEntryPoint(); + } + } + } +#endif // FEATURE_INTERPRETER && FEATURE_PORTABLE_ENTRYPOINTS +} #endif // TARGET_WASM #endif // DACCESS_COMPILE diff --git a/src/coreclr/vm/readytoruninfo.h b/src/coreclr/vm/readytoruninfo.h index e6bc2db3a3a923..ea96ba9ca8a822 100644 --- a/src/coreclr/vm/readytoruninfo.h +++ b/src/coreclr/vm/readytoruninfo.h @@ -199,6 +199,9 @@ class ReadyToRunInfo NativeFormat::NativeHashtable m_typeMapAssemblyTargets; PTR_ReadyToRunInfo m_pNextR2RForUnrelatedCode; + // Next entry when this info is attached to a Module as a supplemental (lazily downloaded) R2R + // image. An info can be on at most one Module's supplemental list. + PTR_ReadyToRunInfo m_pNextSupplemental; TADDR m_pLoadedImageBase; public: @@ -209,6 +212,10 @@ class ReadyToRunInfo PTR_ReadyToRunInfo GetNextUnrelatedR2RModule() { LIMITED_METHOD_CONTRACT; return dac_cast(dac_cast(m_pNextR2RForUnrelatedCode) & ~0x1); } void RegisterUnrelatedR2RModule(); + PTR_Module GetModule() const { LIMITED_METHOD_DAC_CONTRACT; return m_pModule; } + PTR_ReadyToRunInfo GetNextSupplemental() const { LIMITED_METHOD_DAC_CONTRACT; return m_pNextSupplemental; } + void SetNextSupplemental(PTR_ReadyToRunInfo pNext) { LIMITED_METHOD_CONTRACT; m_pNextSupplemental = pNext; } + static PTR_ReadyToRunInfo Initialize(Module * pModule, AllocMemTracker *pamTracker); bool IsComponentAssembly() const { return m_isComponentAssembly; } @@ -246,7 +253,15 @@ class ReadyToRunInfo _ASSERTE(r2rFunctionIndex < m_nRuntimeFunctions); return (PCODE)(GetMinVirtualIP() + RUNTIME_FUNCTION__BeginAddress(&m_pRuntimeFunctions[r2rFunctionIndex])); } - void RegisterVirtualIPRange(Module* pModule); + void RegisterVirtualIPRange(); + + // Attach a lazily-downloaded R2R code supplement (a webcil composite-of-one whose single component + // is pModule's assembly) as a supplemental ReadyToRunInfo on pModule. Runs at a quiesce point. + static ReadyToRunInfo *AttachSupplemental(Module *pModule, NativeImage *pLazyImage, AllocMemTracker *pamTracker); + + // Re-point already-loaded, interpreter-resolved instances of the methods this (supplemental) image + // provides so their next call re-runs the prestub and picks up the newly attached native code. + void RebindLoadedInterpretedMethods(); #endif // TARGET_WASM void RegisterResumptionStub(PCODE stubEntryPoint); diff --git a/src/coreclr/vm/siginfo.cpp b/src/coreclr/vm/siginfo.cpp index 05c245f78cfc8f..2625358cbd9e93 100644 --- a/src/coreclr/vm/siginfo.cpp +++ b/src/coreclr/vm/siginfo.cpp @@ -1298,7 +1298,7 @@ TypeHandle SigPointer::GetTypeHandleThrowing( else #endif { - pModule = pZapSigContext->GetZapSigModule()->GetModuleFromIndex(ix); + pModule = pZapSigContext->GetZapSigModule()->GetModuleFromIndex(ix, pZapSigContext->pR2RInfo); } if (pModule != NULL) diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index 998c970e90a5a6..11a0c66fa32121 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -10,6 +10,7 @@ #include "callingconvention.h" #include "cgensys.h" #include "readytorun.h" +#include "nativeimage.h" #define WASM_STRINGIFY_HELPER(value) #value #define WASM_STRINGIFY(value) WASM_STRINGIFY_HELPER(value) @@ -1351,7 +1352,8 @@ namespace } // Returns null if no thunk is registered for this key yet; the caller then defers. - return LookupPortableEntryPointThunk(keyBuffer); + void* pepThunk = LookupPortableEntryPointThunk(keyBuffer); + return pepThunk; } static ULONG GetHashCode(MethodDesc* pMD, SString &strSource) @@ -1952,3 +1954,104 @@ RtlVirtualUnwind ( return nullptr; } + +#ifdef FEATURE_READYTORUN +// Attach a lazily-downloaded R2R code supplement to a loaded assembly. The payload is the webcil +// composite-of-one already resident at payloadPtr (as produced by the host loader for a ".r2r.wasm" +// asset). Called from the browser host at a quiesce point (from the fetch continuation, so there are no +// managed frames of the target module on the stack). Returns 0 on success, a negative code on failure; +// failure is always non-fatal (the app keeps running with the eager partial image). +extern "C" int32_t CoreCLR_AttachLazyR2RImage(const char *assemblySimpleName, void *payloadPtr, int32_t payloadSize) +{ + if (assemblySimpleName == NULL || payloadPtr == NULL || payloadSize <= 0) + return -1; + + HRESULT hr = S_OK; + Thread *pThread = SetupThreadNoThrow(&hr); + if (pThread == NULL) + return -2; + + int32_t result = 0; + EX_TRY + { + Module *pTargetModule = NULL; + AppDomain::AssemblyIterator it = AppDomain::GetCurrentDomain()->IterateAssembliesEx( + (AssemblyIterationFlags)(kIncludeLoaded | kIncludeExecution)); + CollectibleAssemblyHolder pAssembly; + while (it.Next(pAssembly.This())) + { + Module *pModule = pAssembly->GetModule(); + // Match by simple name only. The eager image may be IL-only (an empty profile compiles no + // methods), so it is not necessarily IsReadyToRun(); the supplemental attaches regardless. + if (pModule != NULL && + strcmp(pModule->GetSimpleName(), assemblySimpleName) == 0) + { + pTargetModule = pModule; + break; + } + } + + if (pTargetModule == NULL) + { + // The assembly is downloaded and registered but not loaded yet (no type used). Force-load it by + // simple name so its supplement is still attached and ready for future use, rather than + // discarding the downloaded image (we do not know which assemblies the app will touch next). + // A simple-name spec still requires a (zeroed) metadata context, and an explicit binder so it + // resolves against the app's default load context (there is no managed caller on this stack). + AssemblySpec spec; + AssemblyMetaDataInternal asmContext; + memset(&asmContext, 0, sizeof(asmContext)); + spec.Init(assemblySimpleName, &asmContext, NULL, 0, 0); + Assembly *pRoot = AppDomain::GetCurrentDomain()->GetRootAssembly(); + if (pRoot != NULL) + spec.SetExplicitBinder(pRoot->GetPEAssembly()->GetAssemblyBinder()); + Assembly *pForced = spec.LoadAssembly(FILE_LOADED, FALSE /* fThrowOnFileNotFound */); + if (pForced != NULL) + pTargetModule = pForced->GetModule(); + } + + if (pTargetModule == NULL) + { + result = -3; + } + else + { + AllocMemTracker amTracker; + AssemblyBinder *pBinder = pTargetModule->GetPEAssembly()->GetAssemblyBinder(); + LoaderAllocator *pLoaderAllocator = pTargetModule->GetLoaderAllocator(); + + ReadyToRunInfo *pInfo = NULL; + { + // The supplemental attach builds a ReadyToRunInfo for the composite-of-one image, which + // declares MVID dependencies and therefore requires the AppDomain file-load lock to be held + // (the normal composite-load path holds it). We are called from a fetch continuation outside + // that path, so acquire it explicitly around the attach. + AppDomain::LoadLockHolder loadLock(AppDomain::GetCurrentDomain()); + + NativeImage *pLazyImage = NativeImage::OpenFromMemory( + (TADDR)payloadPtr, (uint32_t)payloadSize, assemblySimpleName, pBinder, pLoaderAllocator, &amTracker); + + pInfo = (pLazyImage != NULL) + ? ReadyToRunInfo::AttachSupplemental(pTargetModule, pLazyImage, &amTracker) + : NULL; + } + + if (pInfo == NULL) + result = -4; + else + { + // Commit the attach first: SuppressRelease keeps the image's allocations alive, then the + // rebind (best-effort) re-points already-loaded interpreted methods to the new native code. + amTracker.SuppressRelease(); + pInfo->RebindLoadedInterpretedMethods(); + } + } + } + EX_CATCH + { + result = -100; + } + EX_END_CATCH + return result; +} +#endif // FEATURE_READYTORUN diff --git a/src/coreclr/vm/zapsig.cpp b/src/coreclr/vm/zapsig.cpp index fd68ab6b41a554..dff4903aac65ee 100644 --- a/src/coreclr/vm/zapsig.cpp +++ b/src/coreclr/vm/zapsig.cpp @@ -328,7 +328,7 @@ BOOL ZapSig::GetSignatureForTypeHandle(TypeHandle handle, { DWORD ix = CorSigUncompressData(pSig); CONTRACT_VIOLATION(ThrowsViolation|GCViolation); - pModule = pZapSigContext->GetZapSigModule()->GetModuleFromIndexIfLoaded(ix); + pModule = pZapSigContext->GetZapSigModule()->GetModuleFromIndexIfLoaded(ix, pZapSigContext->pR2RInfo); if (pModule == NULL) return FALSE; else @@ -559,7 +559,8 @@ BOOL ZapSig::CompareTypeHandleFieldToTypeHandle(TypeHandle *pTypeHnd, TypeHandle #ifndef DACCESS_COMPILE ModuleBase *ZapSig::DecodeModuleFromIndex(Module *fromModule, - DWORD index) + DWORD index, + ReadyToRunInfo *pInfo) { CONTRACTL { @@ -570,7 +571,8 @@ ModuleBase *ZapSig::DecodeModuleFromIndex(Module *fromModule, CONTRACTL_END; Assembly *pAssembly = NULL; - NativeImage *nativeImage = fromModule->GetCompositeNativeImage(); + ReadyToRunInfo *pR2RInfo = (pInfo != NULL) ? pInfo : fromModule->GetReadyToRunInfo(); + NativeImage *nativeImage = (pInfo != NULL) ? pInfo->GetNativeImage() : fromModule->GetCompositeNativeImage(); uint32_t assemblyRefMax = (nativeImage != NULL ? 0 : fromModule->GetAssemblyRefMax()); if (index <= assemblyRefMax) @@ -588,16 +590,16 @@ ModuleBase *ZapSig::DecodeModuleFromIndex(Module *fromModule, { index -= assemblyRefMax; - if (fromModule->GetReadyToRunInfo()->IsImageVersionAtLeast(6,3)) + if (pR2RInfo->IsImageVersionAtLeast(6,3)) { if (index == 1) { - return fromModule->GetReadyToRunInfo()->GetNativeManifestModule(); + return pR2RInfo->GetNativeManifestModule(); } index--; } - pAssembly = fromModule->GetNativeMetadataAssemblyRefFromCache(index); + pAssembly = (pInfo != NULL) ? NULL : fromModule->GetNativeMetadataAssemblyRefFromCache(index); if(pAssembly == NULL) { @@ -614,7 +616,10 @@ ModuleBase *ZapSig::DecodeModuleFromIndex(Module *fromModule, pParentAssembly); pAssembly = spec.LoadAssembly(FILE_LOADED); } - fromModule->SetNativeMetadataAssemblyRefInCache(index, pAssembly); + if (pInfo == NULL) + { + fromModule->SetNativeMetadataAssemblyRefInCache(index, pAssembly); + } } } @@ -622,7 +627,8 @@ ModuleBase *ZapSig::DecodeModuleFromIndex(Module *fromModule, } ModuleBase *ZapSig::DecodeModuleFromIndexIfLoaded(Module *fromModule, - DWORD index) + DWORD index, + ReadyToRunInfo *pInfo) { CONTRACTL { @@ -634,7 +640,8 @@ ModuleBase *ZapSig::DecodeModuleFromIndexIfLoaded(Module *fromModule, Assembly *pAssembly = NULL; mdAssemblyRef tkAssemblyRef; - NativeImage *nativeImage = fromModule->GetCompositeNativeImage(); + ReadyToRunInfo *pR2RInfo = (pInfo != NULL) ? pInfo : fromModule->GetReadyToRunInfo(); + NativeImage *nativeImage = (pInfo != NULL) ? pInfo->GetNativeImage() : fromModule->GetCompositeNativeImage(); uint32_t assemblyRefMax = (nativeImage != NULL ? 0 : fromModule->GetAssemblyRefMax()); if (index <= assemblyRefMax) @@ -652,16 +659,16 @@ ModuleBase *ZapSig::DecodeModuleFromIndexIfLoaded(Module *fromModule, { index -= assemblyRefMax; - if (fromModule->GetReadyToRunInfo()->IsImageVersionAtLeast(6,3)) + if (pR2RInfo->IsImageVersionAtLeast(6,3)) { if (index == 1) { - return fromModule->GetReadyToRunInfo()->GetNativeManifestModule(); + return pR2RInfo->GetNativeManifestModule(); } index--; } - pAssembly = fromModule->GetNativeMetadataAssemblyRefFromCache(index); + pAssembly = (pInfo != NULL) ? NULL : fromModule->GetNativeMetadataAssemblyRefFromCache(index); if (pAssembly == NULL) { tkAssemblyRef = RidToToken(index, mdtAssemblyRef); @@ -705,7 +712,8 @@ TypeHandle ZapSig::DecodeType(Module *pEncodeModuleContext, ModuleBase *pInfoModule, PCCOR_SIGNATURE pBuffer, ClassLoadLevel level, - PCCOR_SIGNATURE *ppAfterSig /*=NULL*/) + PCCOR_SIGNATURE *ppAfterSig /*=NULL*/, + ReadyToRunInfo *pInfo /*=NULL*/) { CONTRACTL { @@ -717,7 +725,7 @@ TypeHandle ZapSig::DecodeType(Module *pEncodeModuleContext, SigPointer p(pBuffer); - ZapSig::Context zapSigContext(pInfoModule, pEncodeModuleContext); + ZapSig::Context zapSigContext(pInfoModule, pEncodeModuleContext, pInfo); ZapSig::Context * pZapSigContext = &zapSigContext; SigTypeContext typeContext; // empty context is OK: encoding should not contain type variables. @@ -742,12 +750,13 @@ TypeHandle ZapSig::DecodeType(Module *pEncodeModuleContext, MethodDesc *ZapSig::DecodeMethod(Module *pReferencingModule, ModuleBase *pInfoModule, PCCOR_SIGNATURE pBuffer, - TypeHandle * ppTH /*=NULL*/) + TypeHandle * ppTH /*=NULL*/, + ReadyToRunInfo *pInfo /*=NULL*/) { STANDARD_VM_CONTRACT; SigTypeContext typeContext; // empty context is OK: encoding should not contain type variables. - ZapSig::Context zapSigContext(pInfoModule, (void *)pReferencingModule, ZapSig::NormalTokens); + ZapSig::Context zapSigContext(pInfoModule, (void *)pReferencingModule, ZapSig::NormalTokens, pInfo); return DecodeMethod(pInfoModule, pBuffer, &typeContext, &zapSigContext, ppTH, NULL, NULL); } @@ -786,7 +795,7 @@ MethodDesc *ZapSig::DecodeMethod(ModuleBase *pInfoModule, else #endif { - pInfoModule = pZapSigContext->GetZapSigModule()->GetModuleFromIndex(updatedModuleIndex); + pInfoModule = pZapSigContext->GetZapSigModule()->GetModuleFromIndex(updatedModuleIndex, pZapSigContext->pR2RInfo); } } @@ -979,7 +988,8 @@ MethodDesc *ZapSig::DecodeMethod(ModuleBase *pInfoModule, FieldDesc * ZapSig::DecodeField(Module *pReferencingModule, ModuleBase *pInfoModule, PCCOR_SIGNATURE pBuffer, - TypeHandle *ppTH /*=NULL*/) + TypeHandle *ppTH /*=NULL*/, + ReadyToRunInfo *pInfo /*=NULL*/) { CONTRACTL { @@ -991,14 +1001,15 @@ FieldDesc * ZapSig::DecodeField(Module *pReferencingModule, SigTypeContext typeContext; // empty context is OK: encoding should not contain type variables. - return DecodeField(pReferencingModule, pInfoModule, pBuffer, &typeContext, ppTH); + return DecodeField(pReferencingModule, pInfoModule, pBuffer, &typeContext, ppTH, pInfo); } FieldDesc * ZapSig::DecodeField(Module *pReferencingModule, ModuleBase *pInfoModule, PCCOR_SIGNATURE pBuffer, SigTypeContext *pContext, - TypeHandle *ppTH /*=NULL*/) + TypeHandle *ppTH /*=NULL*/, + ReadyToRunInfo *pInfo /*=NULL*/) { CONTRACTL { @@ -1019,7 +1030,7 @@ FieldDesc * ZapSig::DecodeField(Module *pReferencingModule, if (fieldFlags & ENCODE_FIELD_SIG_OwnerType) { - ZapSig::Context zapSigContext(pInfoModule, pReferencingModule); + ZapSig::Context zapSigContext(pInfoModule, pReferencingModule, pInfo); ZapSig::Context * pZapSigContext = &zapSigContext; pOwnerMT = sig.GetTypeHandleThrowing(pInfoModule, diff --git a/src/coreclr/vm/zapsig.h b/src/coreclr/vm/zapsig.h index 31b0f4fab63dd7..d40d341ab21f58 100644 --- a/src/coreclr/vm/zapsig.h +++ b/src/coreclr/vm/zapsig.h @@ -19,6 +19,7 @@ #include "common.h" class SigBuilder; +class ReadyToRunInfo; typedef DWORD(*ENCODEMODULE_CALLBACK)(LPVOID pModuleContext, CORINFO_MODULE_HANDLE moduleHandle); typedef DWORD (*EncodeModuleCallback)(void* pModuleContext, Module *pReferencedModule); @@ -52,22 +53,28 @@ class ZapSig // and is a code:ZapImportTable* when we are running ngen ExternalTokens externalTokens; // When we see a ELEMENT_TYPE_MODULE_ZAPSIG this tells us what type of token follows. + // When non-NULL, ELEMENT_TYPE_MODULE_ZAPSIG module indices are resolved against this (supplemental) + // R2R image's manifest rather than the ZapSig module's primary R2R info. + ReadyToRunInfo * pR2RInfo; + Module * GetZapSigModule() const { return (Module*) pModuleContext; } Context( ModuleBase* _pInfoModule, - void* _pModuleContext, ExternalTokens _externalTokens) + void* _pModuleContext, ExternalTokens _externalTokens, ReadyToRunInfo* _pR2RInfo = NULL) : pInfoModule(_pInfoModule), pModuleContext(_pModuleContext), - externalTokens(_externalTokens) + externalTokens(_externalTokens), + pR2RInfo(_pR2RInfo) { LIMITED_METHOD_CONTRACT; _ASSERTE(externalTokens != IllegalValue); } Context( ModuleBase* _pInfoModule, - Module* _pZapSigModule) + Module* _pZapSigModule, ReadyToRunInfo* _pR2RInfo = NULL) : pInfoModule(_pInfoModule), pModuleContext((void*) _pZapSigModule), - externalTokens(NormalTokens) + externalTokens(NormalTokens), + pR2RInfo(_pR2RInfo) { } }; @@ -136,11 +143,16 @@ class ZapSig //-------------------------------------------------------------------- // Static helper encode/decode helper methods + // When pInfo is non-NULL, native module indices are resolved against that (supplemental) R2R image's + // manifest instead of fromModule's primary R2R info -- used for lazily-attached supplemental images + // whose owning module's primary image is a different (or IL-only) image. static ModuleBase *DecodeModuleFromIndex(Module *fromModule, - DWORD index); + DWORD index, + ReadyToRunInfo *pInfo = NULL); static ModuleBase *DecodeModuleFromIndexIfLoaded(Module *fromModule, - DWORD index); + DWORD index, + ReadyToRunInfo *pInfo = NULL); // referencingModule is the module that references the type. // fromModule is the module in which the type is defined. @@ -151,13 +163,15 @@ class ZapSig ModuleBase *fromModule, PCCOR_SIGNATURE pBuffer, ClassLoadLevel level = CLASS_LOADED, - PCCOR_SIGNATURE *ppAfterSig = NULL); + PCCOR_SIGNATURE *ppAfterSig = NULL, + ReadyToRunInfo *pInfo = NULL); static MethodDesc *DecodeMethod( Module *referencingModule, ModuleBase *fromModule, PCCOR_SIGNATURE pBuffer, - TypeHandle *ppTH = NULL); + TypeHandle *ppTH = NULL, + ReadyToRunInfo *pInfo = NULL); static MethodDesc *DecodeMethod( ModuleBase *pInfoModule, @@ -174,14 +188,16 @@ class ZapSig Module *referencingModule, ModuleBase *fromModule, PCCOR_SIGNATURE pBuffer, - TypeHandle *ppTH = NULL); + TypeHandle *ppTH = NULL, + ReadyToRunInfo *pInfo = NULL); static FieldDesc *DecodeField( Module *pReferencingModule, ModuleBase *pInfoModule, PCCOR_SIGNATURE pBuffer, SigTypeContext *pContext, - TypeHandle *ppTH = NULL); + TypeHandle *ppTH = NULL, + ReadyToRunInfo *pInfo = NULL); static BOOL EncodeMethod( MethodDesc *pMethod, diff --git a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets index a277500ae12aa9..7f3fc3481a0f32 100644 --- a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets +++ b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets @@ -151,6 +151,7 @@ + diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets index cb7de8a0102550..16d88f9a5ba30e 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets @@ -23,12 +23,31 @@ Copyright (c) .NET Foundation. All rights reserved. wasm false - - $(PublishReadyToRunCrossgen2ExtraArgs);--opt-cross-module:* - - $(PublishReadyToRunCrossgen2ExtraArgs);--codegenopt:JitWasmNyiToR2RUnsupported=1;--codegenopt:JitWasmSimdNyiToR2RUnsupported=1 + + <_WasmR2RSharedCrossgen2Args>--opt-cross-module:*;--hard-cross-module-type-refs;--codegenopt:JitWasmNyiToR2RUnsupported=1;--codegenopt:JitWasmSimdNyiToR2RUnsupported=1;--partial-keep-intrinsic-methods + $(PublishReadyToRunCrossgen2ExtraArgs);$(_WasmR2RSharedCrossgen2Args) + + $(PublishReadyToRunCrossgen2ExtraArgs);--partial + + <_WasmLazyR2RCrossgen2Args>$(_WasmR2RSharedCrossgen2Args);--partial-complement;--suppress-component-rewrite + + + + + + + + + <_WasmLazyR2RAllInputs Include="@(_ReadyToRunCompileList)" /> + <_WasmLazyR2RFrameworkRefs Include="@(_ReadyToRunAssembliesToReference)" /> + + + + + + + + <_WasmLazyR2RCompileEntry Include="@(_ReadyToRunCompileList)"> + true + $(_WasmPublishR2RDir)%(_ReadyToRunCompileList.FileName).r2r.wasm + + + <_WasmLazyR2RReferences Include="@(_WasmLazyR2RAllInputs)" Exclude="@(_ReadyToRunCompileList)" /> + <_WasmLazyR2RReferences Include="@(_WasmLazyR2RFrameworkRefs)" /> + + + + + + + + + + + + + + AfterTargets="_CreateR2RImages;_WasmCoreClrCreateLazyR2RImages"> + R2R output .dll), so a surviving .dll would win the probe over the current .wasm. The current + lazy supplements (.r2r.wasm) are excluded so this prune drops only true orphans. --> <_WasmStalePerAppR2R Include="$(_WasmPublishR2RDir)*.wasm;$(_WasmPublishR2RDir)*.dll" - Exclude="@(_ReadyToRunCompileList->'%(OutputR2RImage)')" /> + Exclude="@(_ReadyToRunCompileList->'%(OutputR2RImage)');@(_ReadyToRunCompileList->'$(_WasmPublishR2RDir)%(FileName).r2r.wasm')" /> + + + + <_WasmLazyR2RStagedFile Include="$(_WasmPublishR2RDir)*.r2r.wasm" /> + <_WasmLazyR2RCandidate Include="@(_WasmLazyR2RStagedFile)"> + $(PackageId) + Computed + $(_WasmPublishR2RDir) + $(StaticWebAssetBasePath) + + _framework/%(FileName)%(Extension) + Publish + All + Primary + WasmResource + lazy-r2r + Never + PreserveNewest + %(Identity) + + + <_WasmLazyR2RFingerprintPatterns Include="WasmLazyR2R" Pattern="*.wasm" Expression="#[.{fingerprint}]!" Condition="'$(_WasmFingerprintAssets)' == 'true'" /> + <_WasmLazyR2RFingerprintPatterns Include="WasmLazyR2R" Pattern="*.wasm" Expression="#[.{fingerprint}]?" Condition="'$(_WasmFingerprintAssets)' != 'true'" /> + + + + + + + + + + + + diff --git a/src/native/libs/Common/JavaScript/host/assets.ts b/src/native/libs/Common/JavaScript/host/assets.ts index e70315b5f6332c..a89c8881e98e60 100644 --- a/src/native/libs/Common/JavaScript/host/assets.ts +++ b/src/native/libs/Common/JavaScript/host/assets.ts @@ -46,7 +46,25 @@ export function registerDllBytes(bytes: Uint8Array, virtualPath: string, shortNa } } -export async function instantiateWebcilModule(webcilPromise: Promise, memory: WebAssembly.Memory, virtualPath: string, tableSize?: number, payloadSize?: number): Promise { +// Attach an already-instantiated lazy R2R payload to a loaded assembly by name via the runtime's +// CoreCLR_AttachLazyR2RImage export. Non-fatal: on failure the app keeps running on the eager image. +export function attachLazyR2RImage(payloadPtr: number, payloadSize: number, assemblyName: string): number { + const sp = _ems_.stackSave(); + try { + const nameLen = _ems_.lengthBytesUTF8(assemblyName) + 1; + const namePtr = _ems_.stackAlloc(nameLen); + _ems_.stringToUTF8Array(assemblyName, _ems_.HEAPU8, namePtr as any, nameLen); + const rc = _ems_._CoreCLR_AttachLazyR2RImage(namePtr as any, payloadPtr as any, payloadSize); + if (rc !== 0) { + _ems_.dotnetLogger.warn(`Lazy R2R attach for '${assemblyName}' failed (code ${rc}); continuing with the eager image.`); + } + return rc; + } finally { + _ems_.stackRestore(sp); + } +} + +export async function instantiateWebcilModule(webcilPromise: Promise, memory: WebAssembly.Memory, virtualPath: string, tableSize?: number, payloadSize?: number, lazyR2RAssemblyName?: string): Promise { // The boot config carries payloadSize for every webcil asset (and tableSize for R2R images), so // the loader never buffers the bytes, parses the data section or calls getWebcilSize. Assets // without a tableSize are plain (Webcil wrapper version 0) images. @@ -71,7 +89,7 @@ export async function instantiateWebcilModule(webcilPromise: Promise, const instantiated = await WebAssembly.instantiate(data, imports); instance = instantiated.instance; } - finishWebcilInstance(instance, payloadPtr, payloadSize, tableEntries, virtualPath); + finishWebcilInstance(instance, payloadPtr, payloadSize, tableEntries, virtualPath, lazyR2RAssemblyName); } catch (err) { // Instantiation failed after the payload buffer was allocated; free it to avoid leaking // unmanaged memory. (A grown R2R table cannot be shrunk back, but a failed R2R instantiate is fatal.) @@ -138,9 +156,10 @@ function buildWebcilImports(memory: WebAssembly.Memory, payloadPtr: number, tabl return webcilImports; } -// Copies the payload into the allocated buffer, fills the R2R table (if any) and registers the -// loaded image for BrowserHost_ExternalAssemblyProbe. -function finishWebcilInstance(instance: WebAssembly.Instance, payloadPtr: number, payloadSize: number, tableSize: number, virtualPath: string): void { +// Copies the payload into the allocated buffer, fills the R2R table (if any) and either attaches the +// payload as a lazy R2R code supplement (lazyR2RAssemblyName set) or registers it as a loadable +// assembly for BrowserHost_ExternalAssemblyProbe. +function finishWebcilInstance(instance: WebAssembly.Instance, payloadPtr: number, payloadSize: number, tableSize: number, virtualPath: string, lazyR2RAssemblyName?: string): void { const webcilVersion = (instance.exports.webcilVersion as WebAssembly.Global).value; if (webcilVersion > 1 || webcilVersion < 0) { throw new Error(`Unsupported Webcil version: ${webcilVersion}`); @@ -153,6 +172,13 @@ function finishWebcilInstance(instance: WebAssembly.Instance, payloadPtr: number fillWebcilTable(); } + if (lazyR2RAssemblyName) { + // Lazy R2R supplement: the runtime takes ownership of the payload buffer (process lifetime), + // so it is neither registered as an assembly nor freed here. + attachLazyR2RImage(payloadPtr, payloadSize, lazyR2RAssemblyName); + return; + } + const name = virtualPath.startsWith(browserVirtualAppBase) ? virtualPath.substring(browserVirtualAppBase.length) : virtualPath.substring(virtualPath.lastIndexOf("/") + 1); diff --git a/src/native/libs/Common/JavaScript/loader/assets.ts b/src/native/libs/Common/JavaScript/loader/assets.ts index eaf4508a140905..f5ccd86e7da6af 100644 --- a/src/native/libs/Common/JavaScript/loader/assets.ts +++ b/src/native/libs/Common/JavaScript/loader/assets.ts @@ -261,6 +261,43 @@ export async function fetchSatelliteAssemblies(culturesToLoad: string[]): Promis await Promise.all(promises); } +function lazyR2ROwnerAssemblyName(assetInternal: AssetEntryInternal): string { + // virtualPath names the owning assembly file (e.g. "System.Console.wasm"); strip the extension and + // any directory to get the simple name the runtime resolves the target module by. + const vp = assetInternal.virtualPath ?? assetInternal.name ?? ""; + const withoutExt = vp.replace(/\.(wasm|dll)$/i, ""); + return withoutExt.substring(withoutExt.lastIndexOf("/") + 1); +} + +async function fetchLazyR2R(asset: AssemblyAsset): Promise { + const assetInternal = asset as AssetEntryInternal; + try { + totalAssetsToDownload++; + if (assetInternal.name && !asset.resolvedUrl) { + asset.resolvedUrl = locateFile(assetInternal.name); + } + assetInternal.behavior = "webcil"; + const assemblyName = lazyR2ROwnerAssemblyName(assetInternal); + const webcilPromise = loadResource(assetInternal); + const memory = await wasmMemoryPromiseController.promise; + await dotnetBrowserHostExports.instantiateWebcilModule(webcilPromise, memory, assetInternal.virtualPath ?? assetInternal.name!, assetInternal.tableSize, assetInternal.payloadSize, assemblyName); + } catch (err) { + dotnetLogger.warn(`Lazy R2R supplement '${assetInternal.name}' failed to load: ${err instanceof Error ? err.message : String(err)}`); + } finally { + onDownloadedAsset(assetInternal); + } +} + +// Download and attach all lazy R2R native-code supplements in the background, after the app has +// started. Best-effort: an individual failure leaves the app running on its eager (partial) image. +export async function startLazyR2RDownloads(): Promise { + const lazy = loaderConfig.resources?.lazyR2R; + if (!lazy || lazy.length === 0) { + return; + } + await Promise.all(lazy.map(fetchLazyR2R)); +} + function lazyAssetFileName(virtualPath: string): string { return virtualPath.substring(virtualPath.lastIndexOf("/") + 1); } diff --git a/src/native/libs/Common/JavaScript/loader/dotnet.d.ts b/src/native/libs/Common/JavaScript/loader/dotnet.d.ts index 43637d44c19159..2f097c7059574b 100644 --- a/src/native/libs/Common/JavaScript/loader/dotnet.d.ts +++ b/src/native/libs/Common/JavaScript/loader/dotnet.d.ts @@ -24,6 +24,7 @@ interface EmscriptenModule { _free(ptr: VoidPtr): void; _sbrk(size: number): VoidPtr; _posix_memalign(res: VoidPtrPtr, alignment: number, size: number): number; + _CoreCLR_AttachLazyR2RImage(assemblyNamePtr: CharPtr, payloadPtr: VoidPtr, payloadSize: number): number; out(message: string): void; err(message: string): void; ccall(ident: string, returnType?: string | null, argTypes?: string[], args?: any[], opts?: any): T; @@ -243,6 +244,7 @@ interface Assets { coreAssembly: AssemblyAsset[]; assembly: AssemblyAsset[]; lazyAssembly?: AssemblyAsset[]; + lazyR2R?: AssemblyAsset[]; corePdb?: PdbAsset[]; pdb?: PdbAsset[]; jsModuleDiagnostics?: JsAsset[]; diff --git a/src/native/libs/Common/JavaScript/loader/run.ts b/src/native/libs/Common/JavaScript/loader/run.ts index 6604334d3ce51e..e5503d63819296 100644 --- a/src/native/libs/Common/JavaScript/loader/run.ts +++ b/src/native/libs/Common/JavaScript/loader/run.ts @@ -8,7 +8,7 @@ import { exit, runtimeState } from "./exit"; import { createPromiseCompletionSource } from "./promise-completion-source"; import { getIcuResourceName } from "./icu"; import { loaderConfig, validateLoaderConfig } from "./config"; -import { fetchAssembly, fetchIcu, fetchNativeSymbols, fetchPdb, fetchSatelliteAssemblies, fetchVfs, fetchMainWasm, loadDotnetModule, loadJSModule, nativeModulePromiseController, verifyAllAssetsDownloaded, callLibraryInitializerOnRuntimeReady, callLibraryInitializerOnRuntimeConfigLoaded, prefetchAllResources, prefetchJSModuleLinks, resolveAllDownloadsQueued } from "./assets"; +import { fetchAssembly, fetchIcu, fetchNativeSymbols, fetchPdb, fetchSatelliteAssemblies, fetchVfs, fetchMainWasm, loadDotnetModule, loadJSModule, nativeModulePromiseController, verifyAllAssetsDownloaded, callLibraryInitializerOnRuntimeReady, callLibraryInitializerOnRuntimeConfigLoaded, prefetchAllResources, prefetchJSModuleLinks, resolveAllDownloadsQueued, startLazyR2RDownloads } from "./assets"; import { initPolyfillsLoader } from "./polyfills"; import { validateEngineFeatures } from "./bootstrap"; @@ -187,6 +187,9 @@ export async function createRuntime(downloadOnly: boolean, httpCacheOnly: boolea await Promise.all([...modulesAfterConfigLoadedPromises, ...modulesAfterRuntimeReadyPromises].map(callLibraryInitializerOnRuntimeReady)); + // Background: download and attach lazy R2R native-code supplements while the app already runs. + startLazyR2RDownloads().catch(() => { /* best-effort; failures are logged in the fetch */ }); + } catch (err) { downloadDeferred?.reject(err); exit(1, err); diff --git a/src/native/libs/Common/JavaScript/types/emscripten.ts b/src/native/libs/Common/JavaScript/types/emscripten.ts index 82b49a899982e2..ade0376c8e28a8 100644 --- a/src/native/libs/Common/JavaScript/types/emscripten.ts +++ b/src/native/libs/Common/JavaScript/types/emscripten.ts @@ -31,6 +31,8 @@ export interface EmscriptenModule { _free(ptr: VoidPtr): void; _sbrk(size: number): VoidPtr; _posix_memalign(res: VoidPtrPtr, alignment: number, size: number): number; + // Attach a lazily-downloaded R2R code supplement to a loaded assembly (browser-wasm CoreCLR). + _CoreCLR_AttachLazyR2RImage(assemblyNamePtr: CharPtr, payloadPtr: VoidPtr, payloadSize: number): number; // this should match emcc -s EXPORTED_RUNTIME_METHODS out(message: string): void; diff --git a/src/native/libs/Common/JavaScript/types/public-api.ts b/src/native/libs/Common/JavaScript/types/public-api.ts index ce4f51ef9645ab..a098ea50f6ec9f 100644 --- a/src/native/libs/Common/JavaScript/types/public-api.ts +++ b/src/native/libs/Common/JavaScript/types/public-api.ts @@ -207,6 +207,9 @@ export interface Assets { coreAssembly: AssemblyAsset[]; assembly: AssemblyAsset[]; lazyAssembly?: AssemblyAsset[]; + // Background-downloaded R2R native-code supplements (browser-wasm CoreCLR split). Each entry's + // virtualPath names the owning assembly it attaches to; the route ("name") is ".r2r.wasm". + lazyR2R?: AssemblyAsset[]; corePdb?: PdbAsset[]; pdb?: PdbAsset[]; jsModuleDiagnostics?: JsAsset[]; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs index 0fc20af0914447..b671dc52fa114b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs @@ -10,7 +10,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.Wa /// ExecutionManager::{FindFunctionTableIndexRangeSection, /// GetWasmVirtualIPFromFunctionTableIndex} in src/coreclr/vm/codeman.cpp. It resolves an /// R2R function table entry index against the FunctionTableIndexRangeList to its owning -/// module's , then reads the corresponding +/// image's , then reads the corresponding /// RUNTIME_FUNCTION for the funclet flag, base virtual IP, and unwind data. /// internal sealed class WasmR2RInfo : IWasmR2RInfo @@ -52,8 +52,7 @@ public WasmR2RInfo(Target target) private Data.ReadyToRunInfo GetReadyToRunInfo(Data.FunctionTableIndexRangeSection section) { - Data.Module module = _target.ProcessedData.GetOrAdd(section.R2RModule); - return _target.ProcessedData.GetOrAdd(module.ReadyToRunInfo); + return _target.ProcessedData.GetOrAdd(section.R2RInfo); } private Data.RuntimeFunction GetRuntimeFunction(Data.ReadyToRunInfo r2rInfo, uint localIndex) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/FunctionTableIndexRangeSection.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/FunctionTableIndexRangeSection.cs index ad6875e005d3dc..693ec3696b480a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/FunctionTableIndexRangeSection.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/FunctionTableIndexRangeSection.cs @@ -11,6 +11,6 @@ internal sealed partial class FunctionTableIndexRangeSection : IData? res) AddDictionary(sb, resources.icu); AddDictionary(sb, resources.runtime); AddDictionary(sb, resources.lazyAssembly); + AddDictionary(sb, resources.lazyR2R); if (resources.satelliteResources != null) { @@ -256,6 +257,7 @@ public string TransformResourcesToAssets(BootJsonData config, bool bundlerFriend assets.corePdb = MapGeneralAssets(resources.corePdb); assets.pdb = MapGeneralAssets(resources.pdb); assets.lazyAssembly = MapWebcilAssets(resources.lazyAssembly); + assets.lazyR2R = MapLazyR2RAssets(resources.lazyR2R); if (resources.satelliteResources != null) { @@ -343,6 +345,50 @@ public string TransformResourcesToAssets(BootJsonData config, bool bundlerFriend return asset; }).ToList(); + List? MapLazyR2RAssets(Dictionary? assets) => assets?.Select(a => + { + // The loader derives the owning assembly by stripping the file extension from virtualPath, + // so virtualPath must name the owner ("System.Console.wasm"), not the supplement route + // ("System.Console.r2r.wasm") that name carries. Derive the owner from the DE-fingerprinted + // route so a hashed name (e.g. "System.Console.r2r..wasm") still yields "System.Console.wasm". + string baseRoute = resources.fingerprinting?[a.Key] ?? a.Key; + var asset = new WebcilAsset() + { + virtualPath = DeriveLazyR2ROwner(baseRoute), + name = a.Key, + hash = a.Value, + cache = GetCacheControl(a.Key, resources) + }; + + if (webcilSizes != null && webcilSizes.TryGetValue(a.Key, out var sizes)) + { + asset.payloadSize = sizes.payloadSize; + if (sizes.tableSize > 0) + asset.tableSize = sizes.tableSize; + } + + if (bundlerFriendly) + { + string escaped = EscapeName(a.Key); + imports.Add($"import {escaped} from \"./{a.Key}\";"); + asset.resolvedUrl = EncodeJavascriptVariableInJson(escaped); + } + + return asset; + }).ToList(); + + static string DeriveLazyR2ROwner(string route) + { + // route is ".r2r"; the owner file is "". + const string infix = ".r2r"; + string ext = Path.GetExtension(route); + string withoutExt = route.Substring(0, route.Length - ext.Length); + if (withoutExt.EndsWith(infix, StringComparison.OrdinalIgnoreCase)) + withoutExt = withoutExt.Substring(0, withoutExt.Length - infix.Length); + + return withoutExt + ext; + } + List? MapJsAssets(Dictionary? assets, string? variableNamePrefix = null, string? subFolder = null) => assets?.Select(a => { var asset = new JsAsset() diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs index 7fd50b76f0e224..febc246fe2f194 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs @@ -239,6 +239,14 @@ public class ResourcesData [DataMember(EmitDefaultValue = false)] public ResourceHashesByNameDictionary lazyAssembly { get; set; } + /// + /// ReadyToRun native-code supplements (browser-wasm CoreCLR split) downloaded in the background + /// after startup and attached to their owning eager (partial) assembly. Keyed by the supplement + /// route (e.g. "System.Console.r2r.wasm"). + /// + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary lazyR2R { get; set; } + /// /// JavaScript module initializers that Blazor will be in charge of loading. /// @@ -337,6 +345,15 @@ public class AssetsData [DataMember(EmitDefaultValue = false)] public List lazyAssembly { get; set; } + /// + /// ReadyToRun native-code supplements (browser-wasm CoreCLR split) downloaded in the background + /// after startup and attached to their owning eager (partial) assembly. Each entry's + /// names the owning assembly file so the loader can derive + /// the target module; is the supplement route ("...r2r.wasm"). + /// + [DataMember(EmitDefaultValue = false)] + public List lazyR2R { get; set; } + /// /// JavaScript module initializers that Blazor will be in charge of loading. /// diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs index 90e5608d8d7bc7..ee182ed698d327 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs @@ -382,6 +382,14 @@ private void WriteBootConfig(string entryAssemblyName) AddResourceToList(resource, resourceList, targetPath); continue; } + else if (string.Equals("WasmResource", assetTraitName, StringComparison.OrdinalIgnoreCase) && + string.Equals(assetTraitValue, "lazy-r2r", StringComparison.OrdinalIgnoreCase)) + { + MapFingerprintedAsset(resourceData, resourceRoute, resourceName); + Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a lazy R2R supplement.", resource.ItemSpec); + resourceData.lazyR2R ??= new ResourceHashesByNameDictionary(); + resourceList = resourceData.lazyR2R; + } else if (string.Equals("WasmResource", assetTraitName, StringComparison.OrdinalIgnoreCase) && assetTraitValue.StartsWith("vfs:", StringComparison.OrdinalIgnoreCase)) { Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as VFS resource '{1}'.", resource.ItemSpec, assetTraitValue); diff --git a/src/tests/readytorun/wasm/WasmInterpreterTransitions/WasmInterpreterTransitions.cs b/src/tests/readytorun/wasm/WasmInterpreterTransitions/WasmInterpreterTransitions.cs index 597b80329a84f1..622fb6da9074a3 100644 --- a/src/tests/readytorun/wasm/WasmInterpreterTransitions/WasmInterpreterTransitions.cs +++ b/src/tests/readytorun/wasm/WasmInterpreterTransitions/WasmInterpreterTransitions.cs @@ -208,10 +208,59 @@ public static void TestEntryPoint() Assert.Equal(153, self.InterpretedIntFrom17Int(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)); // IiTiiiiiiiiiiiiiiiip S52 s52 = self.InterpretedInstanceReturnsS52(); Assert.Equal(A, s52.A); Assert.Equal(B, s52.M); // IS52Tp Assert.Equal(unchecked((short)C), InterpretedStaticReturnsS2NoArgs().A); // IS2p + + // Generic-method transitions. A generic method shared across reference-type instantiations + // (the __Canon form) receives a hidden generic-context argument; an exact value-type + // instantiation does not. The R2R call site and the interpreter entry must agree on whether + // that hidden argument is present, or the dispatch traps with a wasm signature mismatch. This + // is the shape AsyncMethodBuilderCore.Start(ref TStateMachine) exercises. + Assert.Equal(A + B, InterpretedGenericIdentity(new Box { Value = A + B }).Value); // R2R -> interp, shared (__Canon) + Assert.Equal(A, InterpretedGenericIdentity(A)); // R2R -> interp, exact value type + Assert.Equal(A + C, self.InterpretedCallsR2RGenericRef()); // interp -> R2R, shared (__Canon) + Assert.Equal(A, self.InterpretedCallsR2RGenericStruct()); // interp -> R2R, exact value type + + // The async kickoff shape: a generic method taking a (struct) state machine by reference. + SmallState st = new SmallState { Value = C }; + InterpretedGenericStart(ref st); // R2R -> interp, ref-struct generic + Assert.Equal(C + A, st.Value); + self.InterpretedCallsR2RGenericStart(ref st); // interp -> R2R, ref-struct generic + Assert.Equal(C + A + B, st.Value); } private static int s_sideEffect; + private sealed class Box { public int Value; } + + private interface IStateLike { void Bump(int by); } + + private struct SmallState : IStateLike { public int Value; public void Bump(int by) => Value += by; } + + [BypassReadyToRun] + [MethodImpl(MethodImplOptions.NoInlining)] + private static T InterpretedGenericIdentity(T value) => value; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static T R2RGenericIdentity(T value) => value; + + [BypassReadyToRun] + [MethodImpl(MethodImplOptions.NoInlining)] + private int InterpretedCallsR2RGenericRef() => R2RGenericIdentity(new Box { Value = A + C }).Value; + + [BypassReadyToRun] + [MethodImpl(MethodImplOptions.NoInlining)] + private int InterpretedCallsR2RGenericStruct() => R2RGenericIdentity(new S16 { A = Wide, B = C }).B == C ? R2RGenericIdentity(A) : 0; + + [BypassReadyToRun] + [MethodImpl(MethodImplOptions.NoInlining)] + private static void InterpretedGenericStart(ref T value) where T : struct, IStateLike => value.Bump(A); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void R2RGenericStart(ref T value) where T : struct, IStateLike => value.Bump(B); + + [BypassReadyToRun] + [MethodImpl(MethodImplOptions.NoInlining)] + private void InterpretedCallsR2RGenericStart(ref SmallState value) => R2RGenericStart(ref value); + // Reverse-pinvoke entry (R2R-compiled) that calls an interpreted static int(int). private static unsafe delegate* unmanaged s_ucoToInterpreted = &UnmanagedCallerCallsInterpreted;