Skip to content

Commit f998c42

Browse files
authored
[generator] [JniConstructorSignature] on bound constructors (#1162)
Context: 3043d89 Context: https://xamarin.github.io/bugzilla-archives/23/2367/bug.html Context: https://github.com/xamarin/monodroid/commit/6679dfc62eae462b5acc9deb095d0fa41786f80b Constructors, how do they work? Commit 3043d89 goes into some details about the interaction between Java constructors and managed-side constructors when the Java constructor is invoked first. What happens when the managed-side constructor is invoked first? class JavaInteropExample : Java.Lang.Object { [JavaCallableConstructor(SuperConstructorExpression="")] public JavaInteropExample (int a, int b) {} } *Before* that code runs (ideally), `jcw-gen` (or equivalent) will run, creating a Java Callable Wrapper for `JavaInteropExample`, and that Java Callable Wrapper (JCW) will contain a constructor: // JCW /* partial */ class JavaInteropExample extends java.lang.Object { public JavaInteropExample(int p0, int p1) { super (); if (getClass () == JavaInteropExample.class) { ManagedPeer.construct (…); } } } Then someone tries: // C# var o = new JavaInteropExample(42); What happens is: 1. `JavaInteropExample(int, int)` constructor begins execution, *immediately* executes (implicit) base constructor invocation `: base()`. 2. `Java.Lang.Object` default constructor executes, which invokes `JavaObject(ref JniObjectReference, JniObjectReferenceOptions)` constructor, which is a no-op as the `JniObjectReference` is invalid. 3. `Java.Lang.Object` default constructor continues, hitting: var peer = JniPeerMembers.InstanceMethods.StartCreateInstance ("()V", GetType (), null); which looks up the Java peer, and invokes the default constructor on the Java peer. 4. `JavaObject.PeerReference` (eventually) is `peer.NewGlobalRef()`, and the `JavaInteropExample` constructor can *now* begin executing. If the JCW doesn't contain a default constructor, then things fail: Error Message: Java.Interop.JavaException : Lnet/dot/jni/test/JavaCallableExample;.<init>()V Stack Trace: at Java.Interop.JniEnvironment.InstanceMethods.GetMethodID(JniObjectReference type, String name, String signature) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/obj/Debug/net7.0/JniEnvironment.g.cs:line 19947 at Java.Interop.JniType.GetConstructor(String signature) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniType.cs:line 182 at Java.Interop.JniPeerMembers.JniInstanceMethods.GetConstructor(String signature) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 63 at Java.Interop.JniPeerMembers.JniInstanceMethods.FinishCreateInstance(String constructorSignature, IJavaPeerable self, JniArgumentValue* parameters) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 173 at Java.Lang.Object..ctor() in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Base/obj/Debug-net7.0/mcw/Java.Lang.Object.cs:line 33 at Java.InteropTests.JavaCallableExample..ctor(Int32 a) in /Users/jon/Developer/src/xamarin/java.interop/tests/Java.Interop.Export-Tests/Java.Interop/JavaCallableExample.cs:line 11 at Java.InteropTests.JavaCallableExampleTest.ManagedCtorInvokesJavaDefaultCtor() in /Users/jon/Developer/src/xamarin/java.interop/tests/Java.Interop.Export-Tests/Java.Interop/JavaCallableExampleTests.cs:line 22 at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr) --- End of managed Java.Interop.JavaException stack trace --- java.lang.NoSuchMethodError: Lnet/dot/jni/test/JavaCallableExample;.<init>()V But why would the JCW for `JavaInteropExample` contain a default constructor at all? In .NET Android, bound constructors have `[Register]`, and `jcw-gen` will emit constructors based on "visible" `[Register]`ed constructors. This ensures that we get *at least one* constructor that exists in Java, which the C# derived types will need to invoke. `generator --codegen-target=JavaInterop1`-style bindings didn't previously emit anything for bound constructors, preventing `jcw-gen` from performing this same logic. Consequently, the JCW for `JavaInteropExample` *didn't* have a default constructor. Add a new `Java.Interop.JniConstructorSignatureAttribute` type, and update `generator` to emit this attribute on bound constructors. Update `jcw-gen` to support `JniConstructorSignatureAttribute`. This adds a default constructor to `JavaInteropExample`: // JCW /* partial */ class JavaInteropExample extends java.lang.Object { public JavaInteropExample() { super (); if (getClass () == JavaInteropExample.class) { ManagedPeer.construct (…); } } public JavaInteropExample(int p0, int p1) { super (); if (getClass () == JavaInteropExample.class) { ManagedPeer.construct (…); } } } Note: while there is a public default constructor on the JCW, Java code cannot call it. If it attempts to do so, an exception will be thrown because the C#-side type doesn't have a default constructor.
1 parent c93fea0 commit f998c42

File tree

21 files changed

+71
-4
lines changed

21 files changed

+71
-4
lines changed

src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,19 @@ void AddConstructor (MethodDefinition ctor, TypeDefinition type, string? outerTy
391391
return r;
392392
}
393393

394+
internal static RegisterAttribute? RegisterFromJniConstructorSignatureAttribute (CustomAttribute attr)
395+
{
396+
// attr.Resolve ();
397+
RegisterAttribute? r = null;
398+
if (attr.ConstructorArguments.Count == 1)
399+
r = new RegisterAttribute (
400+
name: ".ctor",
401+
signature: (string) attr.ConstructorArguments [0].Value,
402+
connector: "",
403+
originAttribute: attr);
404+
return r;
405+
}
406+
394407
internal static RegisterAttribute? RegisterFromJniMethodSignatureAttribute (CustomAttribute attr)
395408
{
396409
// attr.Resolve ();
@@ -461,6 +474,13 @@ static IEnumerable<RegisterAttribute> GetMethodRegistrationAttributes (Mono.Ceci
461474
foreach (var a in GetAttributes<RegisterAttribute> (p, a => ToRegisterAttribute (a))) {
462475
yield return a;
463476
}
477+
foreach (var c in p.GetCustomAttributes ("Java.Interop.JniConstructorSignatureAttribute")) {
478+
var r = RegisterFromJniConstructorSignatureAttribute (c);
479+
if (r == null) {
480+
continue;
481+
}
482+
yield return r;
483+
}
464484
foreach (var c in p.GetCustomAttributes ("Java.Interop.JniMethodSignatureAttribute")) {
465485
var r = RegisterFromJniMethodSignatureAttribute (c);
466486
if (r == null) {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#nullable enable
2+
3+
using System;
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
#if NET
7+
8+
namespace Java.Interop
9+
{
10+
[AttributeUsage (AttributeTargets.Constructor, AllowMultiple = false)]
11+
public sealed class JniConstructorSignatureAttribute : JniMemberSignatureAttribute {
12+
13+
public JniConstructorSignatureAttribute (string memberSignature)
14+
: base (".ctor", memberSignature)
15+
{
16+
}
17+
}
18+
}
19+
20+
#endif // NET

tests/Java.Base-Tests/Java.Base-Tests.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<_Output>-o "$(IntermediateOutputPath)/java"</_Output>
2525
<_Libpath>@(_RefAsmDirs->'-L "%(Identity)"', ' ')</_Libpath>
2626
</PropertyGroup>
27-
<Exec Command="$(DotnetToolPath) $(_JcwGen) &quot;$(TargetPath)&quot; $(_Target) $(_Output) $(_Libpath)" />
27+
<Exec Command="$(DotnetToolPath) $(_JcwGen) -v &quot;$(TargetPath)&quot; $(_Target) $(_Output) $(_Libpath)" />
2828
<Touch Files="$(IntermediateOutputPath)java\.stamp" AlwaysCreate="True" />
2929
</Target>
3030

tests/Java.Interop.Export-Tests/Java.Interop/JavaCallableExampleTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public void JavaCallableWrappers ()
1616
Assert.IsTrue (z);
1717
}
1818

19+
[Test]
20+
public void ManagedCtorInvokesJavaDefaultCtor ()
21+
{
22+
using var o = new JavaCallableExample (42);
23+
}
24+
1925
static JniType CreateUseJavaCallableExampleType () =>
2026
new JniType ("net/dot/jni/test/UseJavaCallableExample");
2127
}

tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteClass.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public partial class MyClass {
88
}
99

1010
// Metadata.xml XPath constructor reference: path="/api/package[@name='java.code']/class[@name='MyClass']/constructor[@name='MyClass' and count(parameter)=0]"
11+
[global::Java.Interop.JniConstructorSignature ("()V")]
1112
unsafe MyClass () : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
1213
{
1314
const string __id = "()V";
@@ -24,6 +25,7 @@ public partial class MyClass {
2425
}
2526

2627
// Metadata.xml XPath constructor reference: path="/api/package[@name='java.code']/class[@name='MyClass']/constructor[@name='MyClass' and count(parameter)=1 and parameter[1][@type='java.lang.String']]"
28+
[global::Java.Interop.JniConstructorSignature ("(Ljava/lang/String;)V")]
2729
unsafe MyClass (string? p0) : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
2830
{
2931
const string __id = "(Ljava/lang/String;)V";

tests/generator-Tests/expected.ji/AccessModifiers/Xamarin.Test.ExtendPublicClass.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ protected ExtendPublicClass (ref JniObjectReference reference, JniObjectReferenc
3030
}
3131

3232
// Metadata.xml XPath constructor reference: path="/api/package[@name='xamarin.test']/class[@name='ExtendPublicClass']/constructor[@name='ExtendPublicClass' and count(parameter)=0]"
33+
[global::Java.Interop.JniConstructorSignature ("()V")]
3334
public unsafe ExtendPublicClass () : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
3435
{
3536
const string __id = "()V";

tests/generator-Tests/expected.ji/AccessModifiers/Xamarin.Test.PublicClass.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ protected PublicClass (ref JniObjectReference reference, JniObjectReferenceOptio
6464
}
6565

6666
// Metadata.xml XPath constructor reference: path="/api/package[@name='xamarin.test']/class[@name='PublicClass']/constructor[@name='PublicClass' and count(parameter)=0]"
67+
[global::Java.Interop.JniConstructorSignature ("()V")]
6768
public unsafe PublicClass () : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
6869
{
6970
const string __id = "()V";

tests/generator-Tests/expected.ji/AccessModifiers/Xamarin.Test.TestClass.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ protected TestClass (ref JniObjectReference reference, JniObjectReferenceOptions
3030
}
3131

3232
// Metadata.xml XPath constructor reference: path="/api/package[@name='xamarin.test']/class[@name='TestClass']/constructor[@name='TestClass' and count(parameter)=0]"
33+
[global::Java.Interop.JniConstructorSignature ("()V")]
3334
public unsafe TestClass () : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
3435
{
3536
const string __id = "()V";

tests/generator-Tests/expected.ji/Constructors/Xamarin.Test.SomeObject.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ protected SomeObject (ref JniObjectReference reference, JniObjectReferenceOption
3030
}
3131

3232
// Metadata.xml XPath constructor reference: path="/api/package[@name='xamarin.test']/class[@name='SomeObject']/constructor[@name='SomeObject' and count(parameter)=0]"
33+
[global::Java.Interop.JniConstructorSignature ("()V")]
3334
[global::System.Obsolete (@"deprecated")]
3435
public unsafe SomeObject () : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
3536
{
@@ -47,6 +48,7 @@ public unsafe SomeObject () : base (ref *InvalidJniObjectReference, JniObjectRef
4748
}
4849

4950
// Metadata.xml XPath constructor reference: path="/api/package[@name='xamarin.test']/class[@name='SomeObject']/constructor[@name='SomeObject' and count(parameter)=1 and parameter[1][@type='int']]"
51+
[global::Java.Interop.JniConstructorSignature ("(I)V")]
5052
public unsafe SomeObject (int aint) : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
5153
{
5254
const string __id = "(I)V";

tests/generator-Tests/expected.ji/Constructors/Xamarin.Test.SomeObject2.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ internal SomeObject2 (ref JniObjectReference reference, JniObjectReferenceOption
3030
}
3131

3232
// Metadata.xml XPath constructor reference: path="/api/package[@name='xamarin.test']/class[@name='SomeObject2']/constructor[@name='SomeObject2' and count(parameter)=1 and parameter[1][@type='int']]"
33+
[global::Java.Interop.JniConstructorSignature ("(I)V")]
3334
public unsafe SomeObject2 (int aint) : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
3435
{
3536
const string __id = "(I)V";

tests/generator-Tests/expected.ji/Streams/Java.IO.FilterOutputStream.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ protected FilterOutputStream (ref JniObjectReference reference, JniObjectReferen
3030
}
3131

3232
// Metadata.xml XPath constructor reference: path="/api/package[@name='java.io']/class[@name='FilterOutputStream']/constructor[@name='FilterOutputStream' and count(parameter)=1 and parameter[1][@type='java.io.OutputStream']]"
33+
[global::Java.Interop.JniConstructorSignature ("(Ljava/io/OutputStream;)V")]
3334
public unsafe FilterOutputStream (global::Java.IO.OutputStream @out) : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
3435
{
3536
const string __id = "(Ljava/io/OutputStream;)V";

tests/generator-Tests/expected.ji/Streams/Java.IO.InputStream.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ protected InputStream (ref JniObjectReference reference, JniObjectReferenceOptio
3030
}
3131

3232
// Metadata.xml XPath constructor reference: path="/api/package[@name='java.io']/class[@name='InputStream']/constructor[@name='InputStream' and count(parameter)=0]"
33+
[global::Java.Interop.JniConstructorSignature ("()V")]
3334
public unsafe InputStream () : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
3435
{
3536
const string __id = "()V";

tests/generator-Tests/expected.ji/Streams/Java.IO.OutputStream.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ protected OutputStream (ref JniObjectReference reference, JniObjectReferenceOpti
3030
}
3131

3232
// Metadata.xml XPath constructor reference: path="/api/package[@name='java.io']/class[@name='OutputStream']/constructor[@name='OutputStream' and count(parameter)=0]"
33+
[global::Java.Interop.JniConstructorSignature ("()V")]
3334
public unsafe OutputStream () : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
3435
{
3536
const string __id = "()V";

tests/generator-Tests/expected.ji/TestInterface/ClassWithoutNamespace.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ protected ClassWithoutNamespace (ref JniObjectReference reference, JniObjectRefe
2828
}
2929

3030
// Metadata.xml XPath constructor reference: path="/api/package[@name='']/class[@name='ClassWithoutNamespace']/constructor[@name='ClassWithoutNamespace' and count(parameter)=0]"
31+
[global::Java.Interop.JniConstructorSignature ("()V")]
3132
public unsafe ClassWithoutNamespace () : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
3233
{
3334
const string __id = "()V";

tests/generator-Tests/expected.ji/TestInterface/Test.ME.GenericImplementation.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ protected GenericImplementation (ref JniObjectReference reference, JniObjectRefe
3030
}
3131

3232
// Metadata.xml XPath constructor reference: path="/api/package[@name='test.me']/class[@name='GenericImplementation']/constructor[@name='GenericImplementation' and count(parameter)=0]"
33+
[global::Java.Interop.JniConstructorSignature ("()V")]
3334
public unsafe GenericImplementation () : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
3435
{
3536
const string __id = "()V";

tests/generator-Tests/expected.ji/TestInterface/Test.ME.GenericObjectPropertyImplementation.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ protected GenericObjectPropertyImplementation (ref JniObjectReference reference,
3030
}
3131

3232
// Metadata.xml XPath constructor reference: path="/api/package[@name='test.me']/class[@name='GenericObjectPropertyImplementation']/constructor[@name='GenericObjectPropertyImplementation' and count(parameter)=0]"
33+
[global::Java.Interop.JniConstructorSignature ("()V")]
3334
public unsafe GenericObjectPropertyImplementation () : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
3435
{
3536
const string __id = "()V";

tests/generator-Tests/expected.ji/TestInterface/Test.ME.GenericStringImplementation.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ protected GenericStringImplementation (ref JniObjectReference reference, JniObje
3030
}
3131

3232
// Metadata.xml XPath constructor reference: path="/api/package[@name='test.me']/class[@name='GenericStringImplementation']/constructor[@name='GenericStringImplementation' and count(parameter)=0]"
33+
[global::Java.Interop.JniConstructorSignature ("()V")]
3334
public unsafe GenericStringImplementation () : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
3435
{
3536
const string __id = "()V";

tests/generator-Tests/expected.ji/TestInterface/Test.ME.GenericStringPropertyImplementation.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ protected GenericStringPropertyImplementation (ref JniObjectReference reference,
3030
}
3131

3232
// Metadata.xml XPath constructor reference: path="/api/package[@name='test.me']/class[@name='GenericStringPropertyImplementation']/constructor[@name='GenericStringPropertyImplementation' and count(parameter)=0]"
33+
[global::Java.Interop.JniConstructorSignature ("()V")]
3334
public unsafe GenericStringPropertyImplementation () : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
3435
{
3536
const string __id = "()V";

tests/generator-Tests/expected.ji/TestInterface/Test.ME.TestInterfaceImplementation.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ protected TestInterfaceImplementation (ref JniObjectReference reference, JniObje
4949
}
5050

5151
// Metadata.xml XPath constructor reference: path="/api/package[@name='test.me']/class[@name='TestInterfaceImplementation']/constructor[@name='TestInterfaceImplementation' and count(parameter)=0]"
52+
[global::Java.Interop.JniConstructorSignature ("()V")]
5253
public unsafe TestInterfaceImplementation () : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
5354
{
5455
const string __id = "()V";

tools/generator/SourceWriters/Attributes/RegisterAttr.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ private void WriteJavaInterop1Attribute (CodeWriter writer)
6666
case MemberTypes.TypeInfo:
6767
writer.WriteLine ($"[global::Java.Interop.JniTypeSignature (\"{Name}\", GenerateJavaPeer={(DoNotGenerateAcw ? "false" : "true")})]");
6868
break;
69+
case MemberTypes.Constructor:
70+
writer.WriteLine ($"[global::Java.Interop.JniConstructorSignature (\"{Signature}\")]");
71+
break;
6972
case MemberTypes.Method:
7073
writer.WriteLine ($"[global::Java.Interop.JniMethodSignature (\"{Name}\", \"{Signature}\")]");
7174
break;

tools/generator/SourceWriters/BoundConstructor.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Reflection;
45
using System.Text;
56
using System.Threading.Tasks;
67
using MonoDroid.Generation;
@@ -29,9 +30,9 @@ public BoundConstructor (ClassGen klass, Ctor constructor, bool useBase, CodeGen
2930

3031
SourceWriterExtensions.AddSupportedOSPlatform (Attributes, constructor, opt);
3132

32-
if (opt.CodeGenerationTarget != CodeGenerationTarget.JavaInterop1) {
33-
Attributes.Add (new RegisterAttr (".ctor", constructor.JniSignature, string.Empty, additionalProperties: constructor.AdditionalAttributeString ()));
34-
}
33+
Attributes.Add (new RegisterAttr (".ctor", constructor.JniSignature, string.Empty, additionalProperties: constructor.AdditionalAttributeString ()) {
34+
MemberType = opt.CodeGenerationTarget != CodeGenerationTarget.JavaInterop1 ? null : (MemberTypes?) MemberTypes.Constructor,
35+
});
3536

3637
SourceWriterExtensions.AddObsolete (Attributes, constructor.Deprecated, opt, deprecatedSince: constructor.DeprecatedSince);
3738
SourceWriterExtensions.AddRestrictToWarning (Attributes, constructor.AnnotatedVisibility, false, opt);

0 commit comments

Comments
 (0)