Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eng/native.wasm.targets
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
<EmccExportedFunction Include="___stack_pointer" />
<EmccExportedFunction Include="___coreclr_wasm_rtlrestorecontext_tag" />
<EmccExportedFunction Include="___async_continuation" />
<EmccExportedFunction Include="_CoreCLR_AttachLazyR2RImage" />
</ItemGroup>
<PropertyGroup>
<_EmccExportedRuntimeMethods>@(EmccExportedRuntimeMethod -> '%(Identity)',',')</_EmccExportedRuntimeMethods>
Expand Down
47 changes: 47 additions & 0 deletions eng/testing/tests.browser.targets
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,53 @@
</ItemGroup>
</Target>

<!-- CoreCLR-only, test-infra-only. The shipped _WasmCoreClrStageLazyR2R stages the lazy R2R complement
supplements (obj/R2R/*.r2r.wasm) only into the nested PUBLISH pass, as AssetKind=Publish /
CopyToOutputDirectory=Never static web assets. Real apps serve the publish output so that is correct,
but library tests serve the OUTER BUILD output (bin/.../wwwroot/_framework) and generate the served boot
config from the build pass (_WriteBuildWasmBootJsonFile over @(WasmStaticWebAsset)). The complement is a
distinct item group the sticky ReferenceCopyLocalPaths/WasmAssembliesFinal restore above never touches,
so it is absent from the served folder and the build boot config lists an empty lazyR2R. Re-stage it here
in the outer build as WasmStaticWebAsset lazy-r2r assets that copy to the output dir and feed the build
boot json - mirroring how the eager crossgen'd set flows back. -->
<Target Name="_WasmCoreClrStageLazyR2RForTests"
Condition="'$(RuntimeFlavor)' == 'CoreCLR' and '$(_WasmEnableWebcil)' == 'true' and '$(PublishReadyToRun)' == 'true' and '$(PublishReadyToRunComposite)' != 'true' and '$(WasmBuildingForNestedPublish)' != 'true' and '$(_WasmFrameworkCopyToOutputDirectory)' == 'PreserveNewest'"
DependsOnTargets="_WasmCoreClrSelectR2RDirectories"
BeforeTargets="_ResolveBuildWasmBootJsonEndpoints">
<ItemGroup Condition="'$(_WasmPublishR2RDir)' != '' and Exists('$(_WasmPublishR2RDir)')">
<_WasmLazyR2RTestStagedFile Include="$(_WasmPublishR2RDir)*.r2r.wasm" />
<_WasmLazyR2RTestCandidate Include="@(_WasmLazyR2RTestStagedFile)">
<SourceId>$(PackageId)</SourceId>
<SourceType>Computed</SourceType>
<ContentRoot>$(_WasmPublishR2RDir)</ContentRoot>
<BasePath>$(StaticWebAssetBasePath)</BasePath>
<RelativePath>_framework/%(FileName)%(Extension)</RelativePath>
<AssetKind>All</AssetKind>
<AssetMode>All</AssetMode>
<AssetRole>Primary</AssetRole>
<AssetTraitName>WasmResource</AssetTraitName>
<AssetTraitValue>lazy-r2r</AssetTraitValue>
<CopyToOutputDirectory>$(_WasmFrameworkCopyToOutputDirectory)</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<OriginalItemSpec>%(Identity)</OriginalItemSpec>
</_WasmLazyR2RTestCandidate>
<!-- Same non-fingerprint (?) vs fingerprint (!) rule the shipped staging uses; owner derivation
de-fingerprints the route so "Foo.r2r.<hash>.wasm" still yields owner "Foo.wasm". -->
<_WasmLazyR2RTestFingerprintPatterns Include="WasmLazyR2R" Pattern="*.wasm" Expression="#[.{fingerprint}]!" Condition="'$(_WasmFingerprintAssets)' == 'true'" />
<_WasmLazyR2RTestFingerprintPatterns Include="WasmLazyR2R" Pattern="*.wasm" Expression="#[.{fingerprint}]?" Condition="'$(_WasmFingerprintAssets)' != 'true'" />
</ItemGroup>

<DefineStaticWebAssets Condition="'@(_WasmLazyR2RTestCandidate)' != ''"
CandidateAssets="@(_WasmLazyR2RTestCandidate)"
FingerprintCandidates="$(_WasmFingerprintAssets)"
FingerprintPatterns="@(_WasmLazyR2RTestFingerprintPatterns)">
<Output TaskParameter="Assets" ItemName="WasmStaticWebAsset" />
<Output TaskParameter="Assets" ItemName="_WasmLazyR2RTestStaticWebAsset" />
</DefineStaticWebAssets>
<!-- Endpoints are defined by _ResolveBuildWasmBootJsonEndpoints over @(WasmStaticWebAsset); we hook before
it, so adding to that item group is enough - defining endpoints here would duplicate them. -->
</Target>

<PropertyGroup Condition="'$(RuntimeFlavor)' == 'CoreCLR'">
<WasmTestSupport>true</WasmTestSupport>
<WasmTestExitOnUnhandledError>true</WasmTestExitOnUnhandledError>
Expand Down
7 changes: 5 additions & 2 deletions src/coreclr/inc/webcildecoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,13 @@ class WebcilDecoder
// R2R
// ------------------------------------------------------------

BOOL HasReadyToRunHeader() const;
BOOL IsComponentAssembly() const
{
// A webcil composite's component assemblies carry the R2R COMPONENT flag; the flat FALSE
// default would make PEImageLayout::IsComponentAssembly() (routed here via DECODER_DISPATCH)
// 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;

Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ partial class CompilationModuleGroup
/// <returns>True if the given module versions with the current compilation module group</returns>
public abstract bool VersionsWithModule(ModuleDesc module);

/// <summary>
/// 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.
/// </summary>
/// <param name="module">Defining module of the referenced type</param>
/// <returns>True if the type reference may be hard-bound to the defining module's token</returns>
public virtual bool HardBindTypeReference(ModuleDesc module) => false;

/// <summary>
/// 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MethodDesc> CompiledMethodDefs;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -67,6 +78,18 @@ public abstract class ReadyToRunCompilationModuleGroupBase : CompilationModuleGr
private readonly Func<EcmaMethod, bool> _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)
{
Expand All @@ -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);

Expand Down Expand Up @@ -947,7 +972,7 @@ static bool ComputeInstantiationTypeVersionsWithCode(Func<TypeDesc, bool> versio
}
}

public virtual void ApplyProfileGuidedOptimizationData(ProfileDataManager profileGuidedCompileRestriction, bool makePartial)
public virtual void ApplyProfileGuidedOptimizationData(ProfileDataManager profileGuidedCompileRestriction, ProfileRestrictionMode mode)
{
_profileData = profileGuidedCompileRestriction;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace ILCompiler
public class ReadyToRunSingleAssemblyCompilationModuleGroup : ReadyToRunCompilationModuleGroupBase
{
private ProfileDataManager _profileGuidedCompileRestriction;
private ProfileRestrictionMode _profileRestrictionMode;
private bool _profileGuidedCompileRestrictionSet;

public ReadyToRunSingleAssemblyCompilationModuleGroup(
Expand All @@ -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;
}

Expand All @@ -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()
Expand Down
Loading
Loading