Skip to content
Open
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
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ indent_size = 4
[*.{cs,csx,java,vb,vbx}]
insert_final_newline = true
indent_style = tab
tab_width = 8
indent_size = 8
tab_width = 4
indent_size = 4
max_line_length = 180

# CMake files
Expand Down
18 changes: 18 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@
"clearjdb"
]
}
,
{
"name": "Run Java.Interop Tests (.NET 9)",
"type": "coreclr",
"request": "launch",
"program": "C:/Program Files/dotnet/dotnet.exe",
"args": [
"exec",
"C:/Program Files/dotnet/sdk/10.0.100-rc.1.25419.117/vstest.console.dll",
"--framework:.NETCoreApp,Version=v9.0",
"${workspaceFolder}/external/Java.Interop/bin/Test${input:configuration}-net9.0/Java.Base-Tests.dll",
"--logger:Microsoft.TestPlatform.MSBuildLogger;Verbosity=minimal",
"--nologo",
"--artifactsProcessingMode-collect"
],
"cwd": "${workspaceFolder}/external/Java.Interop",
"console": "integratedTerminal"
}
],
"inputs": [
{
Expand Down
8 changes: 4 additions & 4 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
{
"label": "Build Microsoft.Android.Sdk.Analysis Tests",
"type": "shell",
"windows": { "command": "dotnet-local.cmd build src/Microsoft.Android.Sdk.Analysis/Tests/Microsoft.Android.Sdk.Analysis.Tests.csproj -c ${input:configuration}", },
"windows": { "command": "./dotnet-local.cmd build src/Microsoft.Android.Sdk.Analysis/Tests/Microsoft.Android.Sdk.Analysis.Tests.csproj -c ${input:configuration}", },
"linux": { "command": "./dotnet-local.sh build src/Microsoft.Android.Sdk.Analysis/Tests/Microsoft.Android.Sdk.Analysis.Tests.csproj -c ${input:configuration}",},
"osx": { "command": "./dotnet-local.sh build src/Microsoft.Android.Sdk.Analysis/Tests/Microsoft.Android.Sdk.Analysis.Tests.csproj -c ${input:configuration}",},
"group": {
Expand All @@ -126,7 +126,7 @@
{
"label": "prepare-sample-under-dotnet",
"type": "shell",
"windows": { "command": "dotnet-local.cmd build --no-restore tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -c ${input:configuration} -t:GenerateNuGetConfig -p:AndroidNETTestConfigOutputDir=${workspaceRoot}/samples", },
"windows": { "command": "./dotnet-local.cmd build --no-restore tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -c ${input:configuration} -t:GenerateNuGetConfig -p:AndroidNETTestConfigOutputDir=${workspaceRoot}/samples", },
"linux": { "command": "./dotnet-local.sh build --no-restore tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -c ${input:configuration} -t:GenerateNuGetConfig -p:AndroidNETTestConfigOutputDir=${workspaceRoot}/samples",},
"osx": { "command": "./dotnet-local.sh build --no-restore tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -c ${input:configuration} -t:GenerateNuGetConfig -p:AndroidNETTestConfigOutputDir=${workspaceRoot}/samples",},
"group": {
Expand All @@ -140,7 +140,7 @@
{
"label": "build-sample-under-dotnet",
"type": "shell",
"windows": { "command": "dotnet-local.cmd build ${input:project} -p:Configuration=${input:configuration} -t:${input:target} -bl:${input:target}.binlog", },
"windows": { "command": "./dotnet-local.cmd build ${workspaceFolder}/${input:project} -p:Configuration=${input:configuration} -t:${input:target} -bl:${input:target}.binlog", },
"linux": { "command": "${input:debugbuildtasks} ./dotnet-local.sh build ${input:project} -p:Configuration=${input:configuration} -t:${input:target} -bl:${input:target}.binlog",},
"osx": { "command": "${input:debugbuildtasks} ./dotnet-local.sh build ${input:project} -p:Configuration=${input:configuration} -t:${input:target} -bl:${input:target}.binlog",},
"group": {
Expand All @@ -154,7 +154,7 @@
{
"label": "run-sample-under-dotnet",
"type": "shell",
"windows": { "command": "dotnet-local.cmd build ${input:project} \"-t:Run\" --no-restore -p:TargetFramework=${input:targetframework} -p:Configuration=${input:configuration} -p:AndroidAttachDebugger=${input:attach} -bl:run.binlog", },
"windows": { "command": "./dotnet-local.cmd build ${workspaceFolder}/${input:project} \"-t:Run\" --no-restore -p:TargetFramework=${input:targetframework} -p:Configuration=${input:configuration} -p:AndroidAttachDebugger=${input:attach} -bl:run.binlog", },
"linux": { "command": "${input:debugbuildtasks} ./dotnet-local.sh build ${input:project} \"-t:Run\" --no-restore -p:TargetFramework=${input:targetframework} -p:Configuration=${input:configuration} -p:AndroidAttachDebugger=${input:attach} -bl:run.binlog",},
"osx": { "command": "${input:debugbuildtasks} ./dotnet-local.sh build ${input:project} \"-t:Run\" --no-restore -p:TargetFramework=${input:targetframework} -p:Configuration=${input:configuration} -p:AndroidAttachDebugger=${input:attach} -bl:run.binlog",},
"group": {
Expand Down
260 changes: 260 additions & 0 deletions src/Microsoft.Android.Sdk.ILLink/GenerateTypeMapAttributesStep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using Java.Interop.Tools.Cecil;
using Java.Interop.Tools.TypeNameMappings;
using Mono.Cecil;
using Mono.Cecil.Rocks;
using Mono.Linker;
using Mono.Linker.Steps;

namespace Microsoft.Android.Sdk.ILLink;

/// <summary>
/// Generate TypeMap attributes using the .NET 10 TypeMapAttribute and TypeMapAssociationAttribute
/// </summary>
public class GenerateTypeMapAttributesStep : BaseStep
{
const string TypeMapAttributeTypeName = "System.Runtime.InteropServices.TypeMapAttribute`1";
MethodReference TypeMapAttributeCtor;

const string TypeMapAssociationAttributeTypeName = "System.Runtime.InteropServices.TypeMapAssociationAttribute`1";
MethodReference TypeMapAssociationAttributeCtor;

const string TypeMapProxyAttributeTypeName = "Java.Interop.TypeMapProxyAttribute";
//TypeReference TypeMapProxyAttribute;
MethodReference TypeMapProxyAttributeCtor;

const string TypeMapAssemblyTargetAttributeTypeName = "System.Runtime.InteropServices.TypeMapAssemblyTargetAttribute`1";
MethodReference TypeMapAssemblyTargetAttributeCtor;

const string JavaTypeMapUniverseTypeName = "Java.Lang.Object";
TypeReference JavaTypeMapUniverseType { get; set; }

TypeReference SystemTypeType { get; set; }
TypeReference SystemStringType { get; set; }

AssemblyDefinition AssemblyToInjectTypeMap { get; set; }
AssemblyDefinition MonoAndroidAssembly { get; set; }

void GetTypeMapAttributeReferences (
string attributeTypeName,
Func<MethodDefinition, bool> ctorSelector,
AssemblyDefinition addReferencesTo,
TypeReference typeMapUniverse,
out MethodReference ctor)
{
var typeMapAttributeDefinition = Context.GetType (attributeTypeName);
var attributeType = addReferencesTo.MainModule.ImportReference (typeMapAttributeDefinition.MakeGenericInstanceType (typeMapUniverse));

var typeMapAttributeCtorDefinition = typeMapAttributeDefinition.Methods
.FirstOrDefault (ctorSelector) ?? throw new InvalidOperationException ($"Couldn't find {attributeTypeName}..ctor()");
var typeMapAttributeCtor = new MethodReference (
typeMapAttributeCtorDefinition.Name,
typeMapAttributeCtorDefinition.ReturnType,
attributeType) {
HasThis = typeMapAttributeCtorDefinition.HasThis,
ExplicitThis = typeMapAttributeCtorDefinition.ExplicitThis,
CallingConvention = typeMapAttributeCtorDefinition.CallingConvention,
};
foreach (var param in typeMapAttributeCtorDefinition.Parameters) {
typeMapAttributeCtor.Parameters.Add (new ParameterDefinition (
param.Name,
param.Attributes,
addReferencesTo.MainModule.ImportReference (param.ParameterType)));
}
ctor = addReferencesTo.MainModule.ImportReference (typeMapAttributeCtor);
}

protected override void Process ()
{
AssemblyToInjectTypeMap = Context.Annotations.GetType ().GetMethod ("GetEntryPointAssembly")?.Invoke (Context.Annotations, null) as AssemblyDefinition ?? throw new NotImplementedException ("asdfasdf NoEntryPoint");
var javaTypeMapUniverseTypeDefinition = Context.GetType (JavaTypeMapUniverseTypeName);
JavaTypeMapUniverseType = AssemblyToInjectTypeMap.MainModule.ImportReference (javaTypeMapUniverseTypeDefinition);

GetTypeMapAttributeReferences (TypeMapAttributeTypeName,
m => m.IsConstructor
&& m.Parameters is [
{ ParameterType.FullName: "System.String" },
{ ParameterType.FullName: "System.Type" },
{ ParameterType.FullName: "System.Type" }],
AssemblyToInjectTypeMap,
JavaTypeMapUniverseType,
out TypeMapAttributeCtor);

GetTypeMapAttributeReferences (TypeMapAssociationAttributeTypeName,
m => m.IsConstructor
&& m.Parameters is [
{ ParameterType.FullName: "System.Type" },
{ ParameterType.FullName: "System.Type" }],
AssemblyToInjectTypeMap,
JavaTypeMapUniverseType,
out TypeMapAssociationAttributeCtor);

MonoAndroidAssembly = javaTypeMapUniverseTypeDefinition.Module.Assembly;
GetTypeMapAttributeReferences (TypeMapAssemblyTargetAttributeTypeName,
m => m.IsConstructor
&& m.Parameters is [{ ParameterType.FullName: "System.String" }],
MonoAndroidAssembly,
JavaTypeMapUniverseType,
out TypeMapAssemblyTargetAttributeCtor);

var typeMapProxyAttrTypeDef = Context.GetType (TypeMapProxyAttributeTypeName);
var typeMapProxyAttribute = AssemblyToInjectTypeMap.MainModule.ImportReference (typeMapProxyAttrTypeDef);
var typeMapProxyAttrCtor = typeMapProxyAttrTypeDef.Methods.Single (m => m.IsConstructor);
TypeMapProxyAttributeCtor = AssemblyToInjectTypeMap.MainModule.ImportReference (typeMapProxyAttrCtor);

SystemTypeType = AssemblyToInjectTypeMap.MainModule.ImportReference (Context.GetType ("System.Type"));
SystemStringType = AssemblyToInjectTypeMap.MainModule.ImportReference (Context.GetType ("System.String"));
}


protected override void ProcessAssembly (AssemblyDefinition assembly)
{
foreach (var type in assembly.MainModule.Types) {
ProcessType (assembly, type);
}
}

// Need to find all possible mappings and pick the best before emitting
Dictionary<string, TypeDefinition> externalMappings = new ();
// Need to inject the Proxy type, but cannot modify the assembly while iterating through types
Dictionary<TypeDefinition, TypeDefinition> proxyMappings = new ();
// list of proxy types to inject into AssemblyToInjectInto in EndProcess
List<TypeDefinition> typesToInject = new ();

/// <summary>
/// Selects the best type that would be mapped to in the AndroidValueManager/AndroidTypeManager
/// </summary>
private TypeDefinition PickBestTargetType (TypeDefinition existing, TypeDefinition type)
{
if (type == existing)
return existing;
// Types in Mono.Android assembly should be chosen first
if (existing.Module.Assembly.Name.Name != "Mono.Android" &&
type.Module.Assembly.Name.Name == "Mono.Android") {
return type;
}
// We found the `Invoker` type *before* the declared type
// Fix things up so the abstract type is first, and the `Invoker` is considered a duplicate.
if ((type.IsAbstract || type.IsInterface) &&
!existing.IsAbstract &&
!existing.IsInterface &&
type.IsAssignableFrom ((TypeReference) existing, Context)) {
return type;
}
// we found a generic subclass of a non-generic type
if (type.IsGenericInstance &&
!existing.IsGenericInstance &&
type.IsAssignableFrom ((TypeReference) existing, Context)) {
return type;
}
return existing;
}

/// <summary>
/// Iterates through all types to find types that map to/from java types, and stores
/// them for modifying the assemblies during EndProcess
/// </summary>
private void ProcessType (AssemblyDefinition assembly, TypeDefinition type)
{
if (type.HasJavaPeer (Context)) {
string javaName = JavaNativeTypeManager.ToJniName (type, Context);
if (externalMappings.TryGetValue (javaName, out var existing)) {
externalMappings [javaName] = PickBestTargetType (existing, type);
} else {
externalMappings.Add (javaName, type);
}
Context.LogMessage (MessageContainer.CreateInfoMessage ($"Type '{type.FullName}' has peer '{javaName}'"));
var proxyType = GenerateTypeMapProxyType (javaName, type);
typesToInject.Add (proxyType);
proxyMappings.Add (type, proxyType);
} else {
Context.LogMessage (MessageContainer.CreateInfoMessage ($"Type '{type.FullName}' has no peer"));
}

if (!type.HasNestedTypes)
return;

foreach (TypeDefinition nested in type.NestedTypes)
ProcessType (assembly, nested);
}

CustomAttribute GenerateTypeMapAssociationAttribute (TypeDefinition type, TypeDefinition proxyType)
{
var ca = new CustomAttribute (TypeMapAssociationAttributeCtor);
ca.ConstructorArguments.Add (new (SystemTypeType, AssemblyToInjectTypeMap.MainModule.ImportReference(type)));
ca.ConstructorArguments.Add (new (SystemTypeType, AssemblyToInjectTypeMap.MainModule.ImportReference(proxyType)));
return ca;
}

CustomAttribute GenerateTypeMapAttribute (TypeDefinition type, string javaName)
{
CustomAttribute ca = new (TypeMapAttributeCtor);
ca.ConstructorArguments.Add (new (SystemStringType, javaName));
ca.ConstructorArguments.Add (new (SystemTypeType, AssemblyToInjectTypeMap.MainModule.ImportReference(type)));
ca.ConstructorArguments.Add (new (SystemTypeType, AssemblyToInjectTypeMap.MainModule.ImportReference(type)));
return ca;
}

protected override void EndProcess ()
{
// HACK ALERT
// We override the entry_assembly so that the TypeMapHandler in illink can have a starting point for TypeMapTargetAssemblies.
// Mono.Android should be the entrypoint assembly so that we can call Assembly.SetEntryAssembly() during application initialization.
// TODO:
// - Add support for "EntryPointAssembly"s that don't have a .entrypoint or Main() method
// - Use MSBuild logic to set the EntryPointAssembly to Mono.Android
Context.Annotations.GetType ().GetField ("entry_assembly", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue (Context.Annotations, MonoAndroidAssembly);
foreach (var type in typesToInject) {
AssemblyToInjectTypeMap.MainModule.Types.Add (type);
Debug.Assert (type.Module.Assembly is not null);
}
foreach (var mapping in externalMappings) {
var attr = GenerateTypeMapAttribute (mapping.Value, mapping.Key);
Context.LogMessage (MessageContainer.CreateInfoMessage ($"Injecting [{attr.AttributeType.FullName}({string.Join (", ", attr.ConstructorArguments.Select (caa => caa.ToString ()))})] into {AssemblyToInjectTypeMap.Name}"));
AssemblyToInjectTypeMap.CustomAttributes.Add (attr);
}
foreach (var mapping in proxyMappings) {
var attr = GenerateTypeMapAssociationAttribute (mapping.Key, mapping.Value);
Context.LogMessage (MessageContainer.CreateInfoMessage ($"Injecting [{attr.AttributeType.FullName}({string.Join (", ", attr.ConstructorArguments.Select (caa => caa.ToString ()))})] into {AssemblyToInjectTypeMap.Name}"));
AssemblyToInjectTypeMap.CustomAttributes.Add (attr);
}

AssemblyToInjectTypeMap.Write (Context.GetAssemblyLocation (AssemblyToInjectTypeMap) + Random.Shared.GetHexString (4) + ".injected.dll");

// JNIEnvInit sets Mono.Android as the entrypoint assembly. Forward the typemap logic to the user/custom assembly;
CustomAttribute targetAssembly = new (TypeMapAssemblyTargetAttributeCtor);
targetAssembly.ConstructorArguments.Add (new (SystemStringType, AssemblyToInjectTypeMap.Name.FullName));
MonoAndroidAssembly.CustomAttributes.Add (targetAssembly);
MonoAndroidAssembly.Write (Path.Combine (
Path.GetDirectoryName (Context.GetAssemblyLocation (AssemblyToInjectTypeMap)),
"Mono.Android." + Random.Shared.GetHexString (4) + ".injected.dll"));
}

TypeDefinition GenerateTypeMapProxyType (string javaClassName, TypeDefinition mappedType)
{
StringBuilder mappedName = new (mappedType.Name);
TypeDefinition? declaringType = mappedType;
while (declaringType is not null) {
mappedName.Insert (0, "_");
mappedName.Insert (0, declaringType.Name);
if (declaringType.DeclaringType is null)
break;
declaringType = declaringType.DeclaringType;
}
var proxyType = new TypeDefinition (
mappedType.Module.Assembly.Name.Name + "._." + declaringType.Namespace,
mappedName.ToString () + "_<androidproxy>",
TypeAttributes.Class | TypeAttributes.NotPublic | TypeAttributes.Sealed,
AssemblyToInjectTypeMap.MainModule.TypeSystem.Object);

var ca = new CustomAttribute (TypeMapProxyAttributeCtor);
ca.ConstructorArguments.Add (new CustomAttributeArgument (SystemStringType, javaClassName));
proxyType.CustomAttributes.Add (ca);
return proxyType;
}
}
35 changes: 35 additions & 0 deletions src/Microsoft.Android.Sdk.ILLink/MarkJavaObjects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,41 @@

namespace MonoDroid.Tuner {

/// <summary>
/// Marks managed types and members that participate in Java <-> .NET interop so
/// they are not removed by the linker/trimmer.
///
/// Behavior overview:
/// - For non-framework assemblies only, proactively preserves types which implement
/// Java interop contracts and members that are invoked from Java but may have no
/// static managed references.
/// - Preserves a user-specified HttpMessageHandler type if provided via
/// LinkContext custom data key "AndroidHttpClientHandlerType", keeping its
/// public parameterless constructor.
/// - Preserves custom views referenced from Android layout XML using the map loaded
/// from "AndroidCustomViewMapFile" (LinkContext custom data), keeping required
/// constructors (Context/IAttributeSet[/Int32]) and JNI constructors
/// (IntPtr) and (IntPtr, JniHandleOwnership).
/// - For types implementing Java.Interop interfaces (IJavaObject/IJavaPeerable),
/// preserves interface invoker types (e.g., IFooInvoker), their constructors and
/// invoked methods discovered via [Register] metadata, and the Java interfaces
/// themselves unless DoNotGenerateAcw/GenerateJavaPeer=false is specified.
/// - For user types which override Java-exposed members, preserves the overridden
/// methods (they are callable from Java but often unreferenced in managed code).
/// - For generated *Implementor types (event implementors), preserves "*Handler"
/// methods referenced from Java callbacks.
/// - Honors attributes implementing Java.Interop.IJniNameProviderAttribute as a
/// preservation signal. Android.Runtime.RegisterAttribute is ignored for this
/// purpose other than reading its properties.
///
/// Inputs via LinkContext custom data:
/// - "AndroidHttpClientHandlerType": string assembly-qualified type name to keep.
/// - "AndroidCustomViewMapFile": string path to the custom view map produced at build time.
///
/// This handler registers assembly/type callbacks and marks items via
/// Linker Annotations, effectively seeding the mark graph so MarkStep can
/// retain the necessary interop surface.
/// </summary>
public class MarkJavaObjects : BaseMarkHandler
{
Dictionary<ModuleDefinition, Dictionary<string, TypeDefinition>> module_types = new Dictionary<ModuleDefinition, Dictionary<string, TypeDefinition>> ();
Expand Down
Loading
Loading