Skip to content

Commit 4787e01

Browse files
authored
[Java.Base-Tests] Test Java-to-Managed invocations for Java.Base (#975)
Commit bc5bcf4 (Desktop `Java.Base` binding) had a TODO: > ~~ TODO: Marshal Methods ~~ > > Marshal methods are currently skipped. Java-to-managed invocations > are not currently supported. No marshal methods means no facility for Java-to-managed invocations. Add a new `tests/Java.Base-Tests` unit test assembly, and test Java-to-managed invocations. This requires touching and updating ~everything. 😅 Update `Java.Interop.dll` to contain a new `Java.Interop.JniMethodSignatureAttribute` custom attribute. Update `generator` to begin emitting `[JniMethodSignature]` on bound methods. This is necessary so that `jcw-gen` knows the JNI method signature of Java methods to emit. As part of this, *remove* the `JniTypeSignatureAttr` type added in bc5bcf4; trying to follow the `JniTypeSignatureAttr` pattern for `JniMethodSignatureAttr` would make for more intrusive code changes. Instead, "re-use" the existing `RegisterAttr` type, adding a `RegisterAttr.MemberType` property so that we can select between Android `[Register]` output vs. "Desktop" `[JniTypeSignature]` and `[JniMethodSignature]` output. This in turn requires updating many `generator-Tests` artifacts. Update `Java.Interop.Tools.JavaCallableWrappers` to look for `Java.Interop.JniTypeSignatureAttribute` and `Java.Interop.JniMethodSignatureAttribute`. This allows `jcw-gen Java.Base-Tests.dll` to generate Java Callable Wrappers for `Java.BaseTests.MyRunnable`. Update `Java.Interop.Export.dll` so that `MarshalMemberBuilder` doesn't use `Expression.GetActionType()` or `Expression.GetFuncType()`, as .NET doesn't support the use of generic types in [`Marshal.GetFunctionPointerForDelegate()`][0]. Instead, we need to use `System.Reflection.Emit` to define our own custom delegate types. ~~ Comparison with Android ~~ Android bindings (both Classic Xamarin.Android and .NET SDK for Android) use `generator`-emitted "connector methods" which are specified in the `[Register]` attribute (see also 99897b2): namespace Java.Lang { [Register ("java/lang/Object" DoNotGenerateAcw=true)] partial class Object { [Register ("equals", "(Ljava/lang/Object;)Z", "GetEquals_Ljava_lang_Object_Handler")] public virtual unsafe bool Equals (Java.Lang.Object? obj) => … static Delegate GetEquals_Ljava_lang_Object_Handler() => JNINativeWrapper.CreateDelegate((_JniMarshal_PPL_Z) n_Equals_Ljava_lang_Object_); static bool n_Equals_Ljava_lang_Object_ (IntPtr jnienv, IntPtr native__this, IntPtr native_obj) => … } } `jcw-gen` emits the "connector method" into the resulting Java Callable Wrappers for appropriate subclasses: String __md_methods = "n_equals:(Ljava/lang/Object;)Z:GetEquals_Ljava_lang_Object_Handler\n"; At runtime, `AndroidTypeManager.RegisterNativeMembers()` will lookup `Object.GetEquals_Ljava_lang_Object_Handler()` and invoke it. The returned `Delegate` instance is provided to `JNIEnv::RegisterNatives()`. The problem with this approach is that it's inflexible: there is no way to participate in the "connector method" infrastructure to alter marshaling behavior. If you need custom behavior, e.g. `Android.Graphics.Color` customizations, you need to update `generator` and rebuild the binding assemblies. (This has been "fine" for the past 10+ years, so this hasn't been a deal breaker.) For `Java.Base`, @jonpryor wants to support the custom marshaling infrastructure introduced in 77a6bf8. This would allow types to participate in JNI marshal method ("connector method") generation *at runtime*, allowing specialization based on the current set of types and assemblies. This means we *don't* need to specify a "connector method" in `[JniMethodSignatureAttribute]`, nor generate them. namespace Java.Lang { [JniTypeSignatureAttribute("java/lang/Object", GenerateJavaPeer=false)] partial class Object { [JniMethodSignatureAttribute("equals", "(Ljava/lang/Object;)Z")] public virtual unsafe bool Equals (Java.Lang.Object? obj) => … // No GetEquals_Ljava_lang_Object_Handler() // No n_Equals_Ljava_lang_Object_() } } This should result in smaller binding assemblies. The downside is that runtime costs *increase*, significantly. Instead of looking up and invoking a "connector method", the method must be *generated* via System.Linq.Expressions expression trees, then compiled into IL, then JIT'd, all before it can be used. We have no timing data to indicate how "bad" this overhead will be. As always, the hope is that `tools/jnimarshalmethod-gen` (176240d) can be used to get the "best of both worlds": the flexibility of custom marshalers, without the runtime overhead. But… * #14 * #616 At this point in time, `jnimarshalmethod-gen` *cannot* work under .NET 6. This will need to be addressed in order for custom marshalers to be a useful solution. `Java.Base` will use "connector method"-less bindings to act as a "forcing function" in getting `jnimarshalmethod-gen` working in .NET. [0]: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.getfunctionpointerfordelegate?view=net-6.0
1 parent 5971625 commit 4787e01

File tree

69 files changed

+2464
-201
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+2464
-201
lines changed

Java.Interop.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Interop.Tools.JavaType
105105
EndProject
106106
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Base", "src\Java.Base\Java.Base.csproj", "{30DCECA5-16FD-4FD0-883C-E5E83B11565D}"
107107
EndProject
108+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Base-Tests", "tests\Java.Base-Tests\Java.Base-Tests.csproj", "{CB05E11B-B96F-4179-A4E9-5D6BDE29A8FC}"
109+
EndProject
108110
Global
109111
GlobalSection(SharedMSBuildProjectFiles) = preSolution
110112
src\Java.Interop.NamingCustomAttributes\Java.Interop.NamingCustomAttributes.projitems*{58b564a1-570d-4da2-b02d-25bddb1a9f4f}*SharedItemsImports = 5
@@ -296,6 +298,10 @@ Global
296298
{30DCECA5-16FD-4FD0-883C-E5E83B11565D}.Debug|Any CPU.Build.0 = Debug|Any CPU
297299
{30DCECA5-16FD-4FD0-883C-E5E83B11565D}.Release|Any CPU.ActiveCfg = Release|Any CPU
298300
{30DCECA5-16FD-4FD0-883C-E5E83B11565D}.Release|Any CPU.Build.0 = Release|Any CPU
301+
{CB05E11B-B96F-4179-A4E9-5D6BDE29A8FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
302+
{CB05E11B-B96F-4179-A4E9-5D6BDE29A8FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
303+
{CB05E11B-B96F-4179-A4E9-5D6BDE29A8FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
304+
{CB05E11B-B96F-4179-A4E9-5D6BDE29A8FC}.Release|Any CPU.Build.0 = Release|Any CPU
299305
EndGlobalSection
300306
GlobalSection(SolutionProperties) = preSolution
301307
HideSolutionNode = FALSE
@@ -346,6 +352,7 @@ Global
346352
{B173F53B-986C-4E0D-881C-063BBB116E1D} = {0998E45F-8BCE-4791-A944-962CD54E2D80}
347353
{11942DE9-AEC2-4B95-87AB-CA707C37643D} = {271C9F30-F679-4793-942B-0D9527CB3E2F}
348354
{30DCECA5-16FD-4FD0-883C-E5E83B11565D} = {0998E45F-8BCE-4791-A944-962CD54E2D80}
355+
{CB05E11B-B96F-4179-A4E9-5D6BDE29A8FC} = {271C9F30-F679-4793-942B-0D9527CB3E2F}
349356
EndGlobalSection
350357
GlobalSection(ExtensibilityGlobals) = postSolution
351358
SolutionGuid = {29204E0C-382A-49A0-A814-AD7FBF9774A5}

Makefile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ TESTS = \
3232
bin/Test$(CONFIGURATION)/Java.Interop.Tools.Generator-Tests.dll \
3333
bin/Test$(CONFIGURATION)/Xamarin.SourceWriter-Tests.dll
3434

35+
NET_TESTS = \
36+
bin/Test$(CONFIGURATION)-net6.0/Java.Base-Tests.dll
37+
3538
PTESTS = \
3639
bin/Test$(CONFIGURATION)/Java.Interop-PerformanceTests.dll
3740

@@ -44,6 +47,7 @@ run-all-tests:
4447
r=0; \
4548
$(MAKE) run-tests || r=1 ; \
4649
$(MAKE) run-test-jnimarshal || r=1 ; \
50+
$(MAKE) run-net-tests || r=1 ; \
4751
$(MAKE) run-ptests || r=1 ; \
4852
$(MAKE) run-java-source-utils-tests || r=1 ; \
4953
exit $$r;
@@ -123,6 +127,11 @@ run-tests: $(TESTS) bin/Test$(CONFIGURATION)/$(JAVA_INTEROP_LIB)
123127
$(foreach t,$(TESTS), $(call RUN_TEST,$(t),1)) \
124128
exit $$r;
125129

130+
run-net-tests: $(NET_TESTS) bin/Test$(CONFIGURATION)-net6.0/$(JAVA_INTEROP_LIB)
131+
r=0; \
132+
$(foreach t,$(NET_TESTS), dotnet test $(t) || r=1) \
133+
exit $$r;
134+
126135
run-ptests: $(PTESTS) bin/Test$(CONFIGURATION)/$(JAVA_INTEROP_LIB)
127136
r=0; \
128137
$(foreach t,$(PTESTS), $(call RUN_TEST,$(t))) \

build-tools/automation/templates/core-tests.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ steps:
130130
arguments: bin/Test$(Build.Configuration)$(NetCoreTargetFrameworkPathSuffix)/Java.Interop-PerformanceTests.dll
131131
continueOnError: true
132132

133+
- task: DotNetCoreCLI@2
134+
displayName: 'Tests: Java.Base'
135+
condition: or(eq('${{ parameters.runNativeDotnetTests }}', 'true'), eq('${{ parameters.runNativeTests }}', 'true'))
136+
inputs:
137+
command: test
138+
testRunTitle: Java.Base (net6.0 - ${{ parameters.platformName }})
139+
arguments: bin/Test$(Build.Configuration)$(NetCoreTargetFrameworkPathSuffix)/Java.Base-Tests.dll
140+
continueOnError: true
141+
133142
- task: DotNetCoreCLI@2
134143
displayName: 'Tests: java-source-utils'
135144
inputs:

src/Java.Base-ref.cs

Lines changed: 1664 additions & 31 deletions
Large diffs are not rendered by default.

src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using System.Linq.Expressions;
66
using System.Reflection;
7+
using System.Reflection.Emit;
78
using System.Text;
89

910
using Java.Interop.Expressions;
@@ -241,15 +242,6 @@ public LambdaExpression CreateMarshalToManagedExpression (MethodInfo method, Jav
241242

242243
static Type GetMarshalerType (Type returnType, List<Type> funcTypeParams, Type declaringType)
243244
{
244-
// `mscorlib.dll` & `System.Core.dll` only provide Action<…>/Func<…> types for up to 16 parameters
245-
if (funcTypeParams.Count <= 16) {
246-
if (returnType != null)
247-
funcTypeParams.Add (returnType);
248-
return returnType == null
249-
? Expression.GetActionType (funcTypeParams.ToArray ())
250-
: Expression.GetFuncType (funcTypeParams.ToArray ());
251-
}
252-
253245
// Too many parameters; does a `_JniMarshal_*` type exist in the type's declaring assembly?
254246
funcTypeParams.RemoveRange (0, 2);
255247
var marshalDelegateName = new StringBuilder ();
@@ -265,11 +257,58 @@ static Type GetMarshalerType (Type returnType, List<Type> funcTypeParams, Type d
265257
}
266258

267259
Type marshalDelegateType = declaringType.Assembly.GetType (marshalDelegateName.ToString (), throwOnError: false);
260+
if (marshalDelegateType != null) {
261+
return marshalDelegateType;
262+
}
268263

264+
#if !NET
269265
// Punt?; System.Linq.Expressions will automagically produce the needed delegate type.
270266
// Unfortunately, this won't work with jnimarshalmethod-gen.exe.
271267
return marshalDelegateType;
268+
#else // NET
269+
return CreateMarshalDelegateType (marshalDelegateName.ToString (), returnType, funcTypeParams);
270+
#endif // NET
271+
}
272+
273+
#if NET
274+
static object ab_lock = new object ();
275+
static AssemblyBuilder assemblyBuilder;
276+
static ModuleBuilder moduleBuilder;
277+
static Type[] DelegateCtorSignature;
278+
279+
static Type CreateMarshalDelegateType (string name, Type returnType, List<Type> funcTypeParams)
280+
{
281+
lock (ab_lock) {
282+
if (assemblyBuilder == null) {
283+
var aname = new AssemblyName ("jni-marshal-method-delegates");
284+
assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly (aname, AssemblyBuilderAccess.Run);
285+
moduleBuilder = assemblyBuilder.DefineDynamicModule (aname.Name!);
286+
287+
DelegateCtorSignature = new Type[] {
288+
typeof (object),
289+
typeof (IntPtr)
290+
};
291+
}
292+
funcTypeParams.Insert (0, typeof (IntPtr));
293+
funcTypeParams.Insert (0, typeof (IntPtr));
294+
var typeBuilder = moduleBuilder.DefineType (
295+
name,
296+
TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.AnsiClass | TypeAttributes.AutoClass,
297+
typeof (MulticastDelegate)
298+
);
299+
300+
const MethodAttributes CtorAttributes = MethodAttributes.RTSpecialName | MethodAttributes.HideBySig | MethodAttributes.Public;
301+
const MethodImplAttributes ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed;
302+
const MethodAttributes InvokeAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual;
303+
304+
typeBuilder.DefineConstructor (CtorAttributes, CallingConventions.Standard, DelegateCtorSignature)
305+
.SetImplementationFlags (ImplAttributes);
306+
typeBuilder.DefineMethod ("Invoke", InvokeAttributes, returnType, funcTypeParams.ToArray ())
307+
.SetImplementationFlags (ImplAttributes);
308+
return typeBuilder.CreateTypeInfo ();
309+
}
272310
}
311+
#endif // NET
273312

274313
static char GetJniMarshalDelegateParameterIdentifier (Type type)
275314
{

0 commit comments

Comments
 (0)