Skip to content

Commit

Permalink
[generator] Add support for peerConstructorPartialMethod (#1087)
Browse files Browse the repository at this point in the history
Context: #1085
Context: dotnet/runtime#82121

Some Java objects are *big*, e.g. [`Bitmap`][0] instances, but as
far as MonoVM is concerned, the `Bitmap` instances are *tiny*: a
few pointers, and that's it.  MonoVM doesn't know that it could be
referencing several MB of data in the Java VM.

MonoVM is gaining support for [`GC.AddMemoryPressure()`][1] and
[`GC.RemoveMemoryPressure()`][2], which potentially allows for the
parent of all cross-VM GC integrations: using the `GC` methods to
inform MonoVM of how much non-managed memory has been allocated.
This could allow MonoVM to collect more frequently when total process
memory is low.

How should we call `GC.AddMemoryPressure()` and
`GC.RemoveMemoryPressure()`?

`GC.RemoveMemoryPressure()` is straightforward: a class can override
`Java.Lang.Object.Dispose(bool)`.

`GC.AddMemoryPressure()` is the problem: where and how can it be
called from a class binding?  This is trickier, as there was no way
to have custom code called by the bound type.  Instead, various
forms of "hacky workarounds" are often employed, e.g. copying
`generator`-emitted content into a `partial` class, then using
`metadata` to *prevent* `generator` from binding those members.
It's all around fugly.

Fortunately C# has a solution: [`partial` methods][3]!

Add support for a `peerConstructorPartialMethod` metadata entry,
applicable to `<class/>` elements, which contains the name of a
`partial` method to both declare and invoke from the "peer constructor":

	<attr
	    path="//class[@name='Bitmap']"
	    name="peerConstructorPartialMethod"
	>_OnBitmapCreated</attr>

This will alter our existing "peer constructor" generation code, a'la:

	partial class Bitmap : Java.Lang.Object {
	    internal Bitmap (IntPtr h, JniHandleOwnership t) : base (h, t)
	    {
	    }
	}

to instead become:

	partial class Bitmap : Java.Lang.Object {
	    partial void _OnBitmapCreated ();
	    internal Bitmap (IntPtr h, JniHandleOwnership t) : base (h, t)
	    {
	        _OnBitmapCreated ();
	    }
	}

This allows a hand-written `partial class Bitmap` to do:

        // Hand-written code
        partial class Bitmap {
            int _memoryPressure;

            partial void _OnBitmapCreated ()
            {
                _memoryPressure = ByteCount;
                GC.AddMemoryPressure (_memoryPressure);
            }

            protected override void Dispose (bool disposing)
            {
                if (_memoryPressure != 0) {
                    GC.RemoveMemoryPressure (_memoryPressure);
                    _memoryPressure = 0;
                }
            }
        }

TODO: "extend" this for `<method/>`s as well?

[0]: https://developer.android.com/reference/android/graphics/Bitmap
[1]: https://learn.microsoft.com/dotnet/api/system.gc.addmemorypressure?view=net-7.0
[2]: https://learn.microsoft.com/dotnet/api/system.gc.removememorypressure?view=net-7.0
[3]: https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/partial-method
  • Loading branch information
jonpryor authored Mar 30, 2023
1 parent 8f3fe62 commit 909239d
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 3 deletions.
4 changes: 4 additions & 0 deletions src/Xamarin.SourceWriter/Models/MethodWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class MethodWriter : ISourceWriter, ITakeParameters
public List<string> Body { get; set; } = new List<string> ();
public bool IsSealed { get; set; }
public bool IsStatic { get; set; }
public bool IsPartial { get; set; }
public bool IsPrivate { get => visibility == Visibility.Private; set => visibility = value ? Visibility.Private : Visibility.Default; }
public bool IsProtected { get => visibility == Visibility.Protected; set => visibility = value ? Visibility.Protected : Visibility.Default; }
public bool IsOverride { get; set; }
Expand Down Expand Up @@ -103,6 +104,9 @@ public virtual void WriteSignature (CodeWriter writer)
if (IsUnsafe)
writer.Write ("unsafe ");

if (IsPartial)
writer.Write ("partial ");

WriteReturnType (writer);

if (ExplicitInterfaceImplementation.HasValue ())
Expand Down
28 changes: 28 additions & 0 deletions tests/generator-Tests/Unit-Tests/CodeGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,34 @@ public void CompatVirtualMethod_Class ()
Assert.True (writer.ToString ().NormalizeLineEndings ().Contains ("catch (Java.Lang.NoSuchMethodError) { throw new Java.Lang.AbstractMethodError (__id); }".NormalizeLineEndings ()), $"was: `{writer}`");
}

[Test]
public void PeerConstructorPartialMethod_Class ()
{
var xml = @"<api>
<package name='java.lang' jni-name='java/lang'>
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
</package>
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
<class abstract='false' deprecated='not deprecated'
extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;'
peerConstructorPartialMethod='_OnMyClassCreated'
jni-signature='Lcom/xamarin/android/MyClass;'
name='MyClass'
final='false' static='false' visibility='public'>
</class>
</package>
</api>";

var gens = ParseApiDefinition (xml);
var klass = gens.Single (g => g.Name == "MyClass");

generator.Context.ContextTypes.Push (klass);
generator.WriteType (klass, string.Empty, new GenerationInfo ("", "", "MyAssembly"));
generator.Context.ContextTypes.Pop ();

Assert.True (writer.ToString ().NormalizeLineEndings ().Contains ("partial void _OnMyClassCreated ();".NormalizeLineEndings ()), $"was: `{writer}`");
Assert.True (writer.ToString ().NormalizeLineEndings ().Contains ("{ _OnMyClassCreated (); }".NormalizeLineEndings ()), $"was: `{writer}`");
}
[Test]
public void WriteDuplicateInterfaceEventArgs ()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public static ClassGen CreateClass (XElement pkg, XElement elem, CodeGenerationO
FromXml = true,
IsAbstract = elem.XGetAttribute ("abstract") == "true",
IsFinal = elem.XGetAttribute ("final") == "true",
PeerConstructorPartialMethod = elem.XGetAttribute ("peerConstructorPartialMethod"),
// Only use an explicitly set XML attribute
Unnest = elem.XGetAttribute ("unnest") == "true" ? true :
elem.XGetAttribute ("unnest") == "false" ? false :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ public bool IsExplicitlyImplementedMethod (string sig)

public bool NeedsNew { get; set; }

public string PeerConstructorPartialMethod { get; set; }

protected override bool OnValidate (CodeGenerationOptions opt, GenericParameterDefinitionList type_params, CodeGeneratorContext context)
{
if (validated)
Expand Down
8 changes: 6 additions & 2 deletions tools/generator/SourceWriters/BoundClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,12 @@ void AddBindingInfrastructure (ClassGen klass, CodeGenerationOptions opt)
void AddConstructors (ClassGen klass, CodeGenerationOptions opt, CodeGeneratorContext context)
{
// Add required constructor for all JLO inheriting classes
if (klass.FullName != "Java.Lang.Object" && klass.InheritsObject)
Constructors.Add (new JavaLangObjectConstructor (klass, opt));
if (klass.FullName != "Java.Lang.Object" && klass.InheritsObject) {
if (!string.IsNullOrWhiteSpace (klass.PeerConstructorPartialMethod)) {
Methods.Add (new ConstructorPartialMethod (klass.PeerConstructorPartialMethod));
}
Constructors.Add (new JavaLangObjectConstructor (klass, opt, klass.PeerConstructorPartialMethod));
}

foreach (var ctor in klass.Ctors) {
// Don't bind final or protected constructors
Expand Down
22 changes: 22 additions & 0 deletions tools/generator/SourceWriters/ConstructorPartialMethod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MonoDroid.Generation;
using Xamarin.Android.Binder;
using Xamarin.SourceWriter;

namespace generator.SourceWriters
{
public class ConstructorPartialMethod : MethodWriter
{
public ConstructorPartialMethod (string partialMethodName)
{
Name = partialMethodName;
IsPartial = true;
IsDeclaration = true;
ReturnType = new TypeReferenceWriter ("void");
}
}
}
5 changes: 4 additions & 1 deletion tools/generator/SourceWriters/JavaLangObjectConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace generator.SourceWriters
{
public class JavaLangObjectConstructor : ConstructorWriter
{
public JavaLangObjectConstructor (ClassGen klass, CodeGenerationOptions opt)
public JavaLangObjectConstructor (ClassGen klass, CodeGenerationOptions opt, string callPartialMethod)
{
Name = klass.Name;

Expand All @@ -31,6 +31,9 @@ public JavaLangObjectConstructor (ClassGen klass, CodeGenerationOptions opt)

BaseCall = "base (javaReference, transfer)";
}
if (!string.IsNullOrWhiteSpace (callPartialMethod)) {
Body.Add ($"{callPartialMethod} ();");
}
}
}
}

0 comments on commit 909239d

Please sign in to comment.