-
Notifications
You must be signed in to change notification settings - Fork 64
[Java.Interop] Add RegisterNativeMembers attribute #206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Java.Interop] Add RegisterNativeMembers attribute #206
Conversation
| MethodInfo registerMethod = null; | ||
| foreach (var m in marshalType.GetRuntimeMethods ()) | ||
| if (m.GetCustomAttribute (typeof (RegisterNativeMembersAttribute)) != null) | ||
| registerMethod = m; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fundamental semantic method here: Do we -- should we -- limit to only a single method? That's what this change does: it means a type can only have a single [RegisterNativeMembers] method.
Is that useful? Is it desirable?
It may be useful and desirable, but it's also not supportable. The intention of the [RegisterNativeMembers] method is to call JNIEnv::RegisterNativeMethods() function. JNIEnv::RegisterNativeMethods() can only be called once. (Not strictly true -- you can of course call more than once, but the invocations are not "additive"; they're replacements. The last invocation "wins.")
As such, allowing multiple [RegisterNativeMembers] methods doesn't make sense. It suggests behavior which can't be provided, not unless we change the semantics entirely.
Random musings for other purposes, which allowed me to answer the above question.
Additionally, there's a performance question. The need for [RegisterNativeMembers] is to help improve xamarin-android process startup time. The eventual process would look like:
Packaging time:
- Run
jnimarshalmethod-gen.exeon all assemblies in the app, producing*-JniMarshalMethod.dllassemblies. For example, if the app has aScratch.MyApp.dllassembly, packaging-time would produce aScratch.MyApp-JniMarshalMethod.dllassembly. These new assemblies will be included with the app (fast deployment or inclusion in the.apk). - The
.apkwould contain "normal" Java Callable Wrappers, as currently produced.
Runtime:
- "Eventually," the static constructor for a Java Callable Wrapper will execute, calling
mono.android.Runtime.register(). Runtime.register()isJNIEnv.RegisterJniNatives()JNIEnv.RegisterJniNatives()will "call"JniRuntime.CurrentRuntime.TypeManager.RegisterNativeMembers()-- probably by by havingAndroidTypeManageroverrideAndroidTypeManager.RegisterNativeMembers().AndroidTypeManager.RegisterNativeMembers()will need to:- Try to load the
*-JniMarshalMethod.dllassembly. if it exists, invoke the[RegisterNativeMethods]type located there. - If (4.1) fails, then "fallback" to the current
string.Split()+ Reflection heavyJNIEnv.RegisterJniMethods()implementation.
- Try to load the
The hope is that using jnimarshalmethod-gen.exe and *-JniMarshalMethod.dll assemblies will be faster than what we currently do, by avoiding the need for System.Reflection.Emit.
The question: is it?
Unfortunately we can't answer that question without trying.
Meanwhile, it would also be useful to try to imagine other ways that JNI method registration could be performed. Is going through Type.GetType() + TypeInfo.GetRuntimeMethods() + ... going to be efficient? Or do we need some other mechanism for mapping Java types to managed types?
Maybe what we instead/also need to do is use an "int" mapping:
// Java Callable Wrapper
class MyActivity {
static final int JavaInteropIndex = 0;
static {
Runtime.register (JavaInterpIndex, MyActivity.class);
}
}// helper glue in `Mono.Android.dll`?
struct RegistrationInfo {
Type RuntimeType;
Action<JniType, string> RegistrationMethod;
public RegistrationInfo (Type type, Action<JniType, string> method)
{
RuntimeType = type;
RegistrationMethod = method;
}
}
// ...generated during some packaging step
static class Registrations {
public static RegistrationInfo[] Info = new[]{
/* index 0 */ new RegistrationInfo (typeof (MyActivity), MyActivity.RegisterNativeMethods), // method from jnimarshalmethod-gen
// ...
};
}This would allow some variation on JNIEnv.RegisterJniNatives() to directly turn the int index value from Runtime.register() into an index into Registrations.Info.
This sounds cool.
What's unknown is what the startup runtime/overhead penalty will be for the Registrations static constructor, which will have thousands of typeof() expressions + method lookups (off).
Turning this back to the original question -- should we support more than one [RegisterNativeMethod] method? -- supporting more than one method would in turn require that RegistrationInfo have a Action<JniType, string>[], not a single. Is that meaningful? ¯_(ツ)_/¯
However, supporting more than one method does mean that one could plausibly (but sanely?!) use "custom" method registration alongside jnimarshalmethod-gen-based registration.
...which is exactly the problem, and why this cannot work.
...that said, maybe that's the right thing to do? Instead of a [RegisterNativeMethods]
static void RegisterMethods (JniType type, string members)
{
// ...
type.RegisterNativeMethods (...);
}we could instead do: [JniNativeMethodRegistration]
static IEnumerable<JniNativeMethodRegistration> GetNativeMethods (JniType type, string members)
{
return ...;
}The actual For example, in partial class ExportTest {
[JniNativeMethodRegistration]
static IEnumerable<JniNativeMethodRegistration> RegisterNativeMembers (JniType type, string members)
{
var methods = JniEnvironment.Runtime.MarshalMemberBuilder
.GetExportedMemberRegistrations (typeof (ExportTest));
return methods;
}
}This could plausibly support intermixing registration of methods that static bool TryRegisterNativeMembers (JniType nativeClass, Type marshalType, string methods)
{
var registrations = new List<JniNativeMethodRegistration>();
foreach (var m in marshalType.GetRuntimeMethods ()) {
if (m.GetCustomAttribute (typeof (RegisterNativeMembersAttribute)) == null)
continue;
var reg = (Func<JniType, string, IEnumerable<JniNativeMethodRegistration >>)registerMethod.CreateDelegate (typeof (Func<JniType, string, IEnumerable<JniNativeMethodRegistration >>));
registrations.AddRange (reg (nativeClass, methods));
}
nativeClass.RegisterNativeMethods (registrations.ToArray ());
return true;
} |
struct JniNativeMethodRegistrationArguments {
public ICollection<JniNativeMethodRegistration> Registrations {get;}
public string Methods {get;}
}
class Example {
[JniAddNativeMethodRegistration]
static void Register (JniNativeMethodRegistrationArguments e)
{
e.Registrations.Add (new JniNativeMethodRegistration (...));
}
} |
9d1c03b to
78c220f
Compare
|
build |
| .GetExportedMemberRegistrations (typeof (ExportTest)); | ||
| type.RegisterNativeMethods (methods.ToArray ()); | ||
| args.AddRange (JniEnvironment.Runtime.MarshalMemberBuilder | ||
| .GetExportedMemberRegistrations (typeof (ExportTest))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your editor is weird, and is inserting spaces. This should be two tab stops before the .GetExportedMemberRegistrations().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is VS/Mac with Mono coding style. This case is not defined by http://www.mono-project.com/community/contributing/coding-guidelines/, so I think it tries to align it with the beginning of the parameter.
I will put it just on single line as there are even longer lines in that file.
| { | ||
| public struct JniNativeMethodRegistrationArguments | ||
| { | ||
| public ICollection<JniNativeMethodRegistration> Registrations { get { return _registrations; } } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please format this member conventionally:
public ICollection<JniNativeMethodRegistration> Registrations {
get { return _registrations; }
}| public string Methods { get; } | ||
| List<JniNativeMethodRegistration> _registrations; | ||
|
|
||
| public JniNativeMethodRegistrationArguments (List<JniNativeMethodRegistration> registrations, string methods) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please null-check the arguments.
| Methods = methods; | ||
| } | ||
|
|
||
| public void AddRange (IEnumerable<JniNativeMethodRegistration> registrations) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename as AddRegistrations().
|
|
||
| public void AddRange (IEnumerable<JniNativeMethodRegistration> registrations) | ||
| { | ||
| _registrations.AddRange (registrations); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This type is a struct. As such, it has an implicit default constructor, from which _registrations will be null.
We should choose one of three things:
- Ignore it, and realize that
NullReferenceExceptioncan be thrown from here null-check_registrations, and throwInvalidOperationExceptionwhennull.- Use
_registrations?.AddRange(), and "ignore" it.
I'm inclined toward (3).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, an alternate idea comes to mind:
If the constructor instead took ICollection<JniNativeMethodRegistration>, we could support that by just using .Add() here:
foreach (var v in registrations) {
Registrations.Add (v);
}This would allow removing the Gendarme ignore on the constructor, and would allow non-List<JniNativeMethodRegistration> values to be provided. It just means we have more .Add() calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is how I implemented it first. Later I used List::AddRange to improve performance, which I think is our goal here.
Optionally we can use ICollection<JniNativeMethodRegistration> and try to cast it to List<JniNativeMethodRegistration> and use AddRange in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would help "intrinsic understanding" ("who reads documentation?") if the constructor parameter type matched the property type.
As such, both the constructor parameter and the property should either be ICollection<T> or List<T>. (Not sure how this would impact Gendarme, but for consistency and understanding they should be the same type.)
Given that we're mostly after performance, I think using List<T> everywhere would be better.
We still need to handle/answer the question of "what happens when Registrations is null?"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I changed them both to ICollection<T> and in AddRegistrations I check whether it can be cast to List<T> to speed it up.
When Registrations is null, I made it throw an InvalidOperationException as it is invalid state we would like to avoid. We should not get into such state, so throwing the exception might potentially helps us find a bug.
a25f54e to
ad82b43
Compare
| var lambda = Expression.Lambda<Action<JniType, string>> (body, new[]{ type, members }); | ||
| var lambda = Expression.Lambda<Action<JniNativeMethodRegistrationArguments>> (body, new[]{ args }); | ||
|
|
||
| var rb = dt.DefineMethod ("__RegisterNativeMembers", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably still a good idea to leave this as __RegisterNativeMembers, or use some other "unique" name, because the type generated will also contain names under user control, as the marshal method names mirror the method names to register.
Otherwise we need to rely on the parameter signature being unique, which is also probably fine, but doesn't make me feel entirely good.
Implementation of improvement noted in xamarin#205 comments. Instead of "hard-wiring" the name of a method to register native members in a given type, introduce new attribute to mark appropriate method in that type. It is now possible to have multiple registration methods. The `TestType.cs` is updated to test this new behavior. `JniNativeMethodRegistrationArguments::RegisterNativeMethods` is now internal to avoid calling it from 3rd party registration methods as it would be overriden later and the registrations would be lost. Updated all related code to use the new attribute. It also fixes fixes issues reported by Gendarme, `R: Gendarme.Rules.Performance.AvoidUnusedParametersRule` and gets us to 116 defects reported.
ad82b43 to
db7858b
Compare
Implementation of improvement noted in
#205 comments.
Instead of "hard-wiring" the name of a method to register native
members in a given type, introduce new attribute to mark appropriate
method in that type.
It is now possible to have multiple registration methods. The
TestType.csis updated to test this new behavior.JniNativeMethodRegistrationArguments::RegisterNativeMethodsis nowinternal to avoid calling it from 3rd party registration methods as it
would be overriden later and the registrations would be lost.
Updated all related code to use the new attribute.
It also fixes fixes issues reported by Gendarme,
R: Gendarme.Rules.Performance.AvoidUnusedParametersRuleand gets us to116 defects reported.