Skip to content

TypeMapLazyDictionary expects a single entrypoint assembly, which isn't a valid assumption on iOS and Android #120271

Description

@jtschuster

The TypeMap API expects that there is a single entrypoint assembly from which TypeMapAssemblyTargetAttributes can point to other assemblies to create the entire typemap. This assumption isn't valid on iOS or Android where .NET is called into from native code. See below

RuntimeAssembly? startingAssembly = (RuntimeAssembly?)Assembly.GetEntryAssembly();

cc. @AaronRobinsonMSFT @simonrozsival

#113362 to link the issues

Activity

  1. added this to the 11.0.0 milestone on Sep 30, 2025
  2. dotnet-policy-service commented on Sep 30, 2025

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @dotnet/interop-contrib
    See info in area-owners.md if you want to be subscribed.

  3. AaronRobinsonMSFT commented on Sep 30, 2025

    @AaronRobinsonMSFT
    Member

    @jtschuster Those applications should call Assembly.SetEntryAssembly() with the expected entry assembly. All should be able to determine the "main" on the .NET side. Please let us know if that is not the case.

    We should use this issue to also update the documentation.

  4. simonrozsival commented on Oct 1, 2025

    @simonrozsival
    Member

    @jtschuster @AaronRobinsonMSFT for clarification - TypeMapLazyDictionary is the dynamic implementation that's intended for Debug builds? I ran into this issue with Release builds where I expected ILLink to produce the type map at build-time. That makes me believe there are two problems: the first is at build time looking up the assembly attributes that define the typemap and the other one is at runtime locating the pre-built typemap. As far as I can tell we currently don't call the SetEntryAssembly method in dotnet/android, dotnet/macios, or dotnet/maui, but I'm sure we can add it a nd fix the runtime problem. Now since the managed code in Android and iOS apps are just a bunch of managed libraries and not executable assemblies, I believe there is no "entry point assembly" and so the TypeMapHandler won't find any of the associations:

    public TypeMapHandler(AssemblyDefinition entryPointAssembly)
    {
    HashSet<AssemblyNameReference> assemblies = [AssemblyNameReference.Parse(entryPointAssembly.FullName)];
    foreach (var attr in entryPointAssembly.CustomAttributes)
    {
    if (attr.AttributeType is not GenericInstanceType
    {
    Namespace: "System.Runtime.InteropServices",
    GenericArguments: [_]
    })
    {
    continue; // Only interested in System.Runtime.InteropServices attributes
    }
    if (attr.AttributeType.Name != "TypeMapAssemblyTarget`1"
    || attr.ConstructorArguments[0].Value is not string str)
    {
    // Invalid attribute, skip it.
    // Let the runtime handle the failure.
    continue;
    }
    assemblies.Add(AssemblyNameReference.Parse(str));
    }
    _lazyTypeMapResolver = new TypeMapResolver(assemblies);
    }

    I wonder if it might be simpler to add the option to explicitly pass a list of type map assemblies to illink/ilc and only fall back to the entry-point assembly if none are provided. We could then easily set the path(s) to the relevant assemblies through MSBuild.

  5. AaronRobinsonMSFT commented on Oct 1, 2025

    @AaronRobinsonMSFT
    Member

    I believe there is no "entry point assembly" and so the TypeMapHandler won't find any of the associations

    I'd defer to @jkoritzinsky for the native AOT scenario - I understand your query. The counter to this though is the trimmer logic starts from somewhere. It can be an implicit "main" or the classic explciit Main, but it starts from what is determines to be a root and then walks the method execution tree. There must be some root, implicit or explicit, in those scenarios so the trimmer knows where to start. We simply need to also set that in the above manner so the TypeMap can also know where to start.

  6. jkotas commented on Oct 1, 2025

    @jkotas
    Member

    TypeMapLazyDictionary is the dynamic implementation that's intended for Debug builds?

    The dynamic implementation of TypeMapLazyDictionary is meant to be used for both Debug and Release, except Native AOT.

    it starts from what is determines to be a root and then walks the method execution tree.

    Yes, it happens here in the linker:

    if (Annotations.GetEntryPointAssembly() is AssemblyDefinition entryPoint)
    {
    _typeMapHandler = new TypeMapHandler(entryPoint);
    }

    The entrypoint assembly is required to have Main method

    var ep = assembly.MainModule.EntryPoint;
    if (ep == null)
    {
    Context.LogError(null, DiagnosticId.RootAssemblyDoesNotHaveEntryPoint, assembly.Name.ToString());
    return;
    }
    Annotations.Mark(ep.DeclaringType, di, origin);
    Annotations.AddPreservedMethod(ep.DeclaringType, ep);
    Annotations.SetEntryPointAssembly(assembly);

    The question is whether we relax the requirement for entrypoint assembly to have the Main method so that it works for library-based applications too, or whether to we build a parallel scheme to pass around arbitrary typemap root assembly all the way from msbuild build targets. I think we can start with relaxing the requirement to see whether it works well.

    the native AOT scenario

    This mirrors the IL linker flow. For reference:

    typeMapManager = new UsageBasedTypeMapManager(TypeMapMetadata.CreateFromAssembly(assembly, typeSystemContext));

  7. jtschuster commented on Oct 1, 2025

    @jtschuster
    MemberAuthor

    It looks like the Android SDK sets the TrimmerRootAssemblys (which ends up being the IntermediateAssembly) to have RootMode=All, which would have very different semantics from EntryPoint.

    https://github.com/dotnet/android/blob/f763743128a312a1fb9994e11f26d20c7f0c305b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.ILLink.targets#L105-L109

    case AssemblyRootMode.AllMembers:
    Annotations.SetRootAssembly(assembly);
    Annotations.Mark(assembly.MainModule, di, origin);
    return;

    We could treat all TrimmerRootAssemblys as the roots for the TypeMap without having to add a new option. It doesn't look like it would be too complex to extend the implementation to handle this.

    If we want to enforce a single EntryPoint assembly as the TypeMap root, we'll likely need to support using different RootMode trimming semantics for the EntryPoint assembly (like Library, Visible, All).

  8. simonrozsival commented on Oct 1, 2025

    @simonrozsival
    Member

    It looks like the Android SDK sets the TrimmerRootAssembly

    AFAIK this is something we would like to remove as soon as we can use the type map API.

  9. jtschuster commented on Oct 28, 2025

    @jtschuster
    MemberAuthor

    Closing this and using #121138 to track the trimming work to enable TypeMap on library apps.

  10. locked and limited conversation to collaborators on Nov 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    • Status
      No status

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions