-
Notifications
You must be signed in to change notification settings - Fork 28
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
Proposal: Use "TypeClasses" to instantiate objects / call static methods #723
Comments
Thanks for the proposal @HosseinYousefi! There's definitely an interaction between arrays and generics going on here. I'd like to center the API as most around a single class as possible, and only expose a type class if absolutely necessary.
Assuming that these belong to > MyFoo.newInstance(arg1, arg2, ...);
> MyFoo.newInstance1(arg1, arg2, ...);
> // and so on... With JNI's array API we need to pass something in that can produce the right Java signature string. However, I'd like the API to be structured making that class as invisible as possible: JniArrayTypeClass(MyFoo.type).newInstance(3); The With generics, the JniArrayTypeClass(MyBar.type(MyKey.type,MyValue.type)).newInstance(3); I'd maybe even prefer: JniArray.newInstance(
JniArray.type(
MyBar.type(MyKey.type, MyValue.type),
),
3,
); Actually, maybe we don't even need the runtime String representation of the type arguments, because those are erased right? But having some type arguments be inferred and others not is also slightly ugly: JniArray<MyBar<MyKey, MyValue>> myArray = JniArray.newInstance(
JniArray.type(
MyBar.typeErased,
),
3,
); Then we might as well write out the first option. Do we need the JniArray<MyBar<MyKey, MyValue>> myArray = JniArray.newInstance(
3,
// `type` should then be a named optional argument
); We would only see this stuff on on the arrays, because there we have an API in which we need to pass the type strings. For instantiating an object with generics, we only need to instantiate Dart type arguments. Java has no runtime reification of the type arguments, so we don't have to pass in the class-strings: MyBar<MyKey, MyValue> myBar = MyBar.newInstance(arg1, arg2, ...) I haven't looked at the actual JNI API we're mapping to while writing this reply. So maybe there are more constraints why what I'm proposing wouldn't work. edit: with my request for |
I understand that you want to keep the API simpler, but I feel like we're writing What about only exposing the type class and naming it as we currently name the normal classes without the // MyFoo is actually the TypeClass and newInstance returns MyFooObject
MyFoo().newInstance(...);
MyFoo().invokeStaticMethod(...); final array1 = JniArrayTypeClass(MyBar.type(MyKey.type,MyValue.type)).newInstance(3);
// turns into:
final array2 = JniArray(MyBar(MyKey(), MyValue())).newInstance(3); I'm not sure how the tree-shaking works for // In the generated file:
const MyKey = MyKeyTypeClass();
const MyValue = MyValueTypeClass();
// Somewhere else:
final foo = MyFoo(MyKey, MyValue).newInstance(...); |
Does that mean that if you were to write a Flutter app, where Flutter lints force you to spell out the type your code would look like: final MyFooObject myFoo = MyFoo().newInstance(...); I think having two types would make it rather confusing. Using final MyFoo myFoo = MyFoo.klass.newInstance(...); Same with generics and arrays, they would be forced to be: final JArray<MyFooObject> jArray = ... instead of final JArray<MyFoo> jArray = ... |
Hmm, what about combining the two classes? We're using // Assuming that build returns `void`, could also return `this` I guess
final MyFoo myFoo = MyFoo()..build(...);
final MyFoo myFoo1 = MyFoo()..build1(...);
final bar = MyBar(MyKey(), MyValue())..build(...); This assigns some non-null value to the reference pointer. |
That thought also crossed my mind, that would make the type situation much simpler indeed. But using the normal constructors for these "special" objects pollutes the constructor space. Especially, if a lot of code might not deal with generics or arrays. I'd prefer having constructors over // I'd prefer keeping the non-generic use case as simple as possible.
final MyFoo myFoo = MyFoo(...)
final MyFoo myFoo1 = MyFoo(...);
// Using a named constructor for the special objects has the same effect as a static getter/method
final bar = MyBar(MyKey.klass(), MyValue.klass());
Yeah other programming languages do that often, we don't because we have |
I don't like this because
// java world: MyBar<A, B>
final bar = MyBar(A.klass(), B.klass()); // bar is now null
// java world: MyFoo that has a constructor that gets A and B objects
final foo = MyFoo(A.klass(), B.klass()); // foo is a non-null object that has null properties edit:
final foo = MyFoo()..setContext(...)..build(...); |
Thinking about it, the trick to infer the type arguments from the type object, only works if we use the "special" object. final myList = MyList</*MyFoo inferred*/>.newInstance(MyFoo(), 50); edit: actually that trick can still work with two classes: final myList = MyList</*MyFoo inferred*/>.newInstance(/*extends jni.JniTypeClass<MyFoo>*/ MyFoo.klass, 50); |
Yeah, I don't like that about the "special" objects of a single class either.
That is very bad indeed. I believe your snippet should be: // java world: MyBar<A, B>
final bar = MyBar(A.klass(), B.klass()); // bar is now null
// java world: MyFoo that has a constructor that gets A and B objects
final foo = MyFoo(A(), B()); // foo is a non-null object that has two objects passed to its constructor Indeed, far from desirable. Let me do some prototyping in an editor and let's discuss all the options offline and write some notes here afterwards. |
No, I actually meant the original snippet. We're not constructing A and B, we're passing |
Ah, that's even more confusing indeed. |
Another important thing to consider in our design is supporting subtyping (I believe you already created the issue #764). Currently this happens: // java world: Foo extends Bar
void f(Bar bar) {}
final foo = Foo();
f(foo); // Error! Expected Bar instead of Foo |
Gist from discussion:
Design for in the background: https://gist.github.com/dcharkes/31351012ff4c67884bd406235ef9c571 |
I skimmed over previous discussion. So correct me if I got something wrong. Alternatively, you can keep the current design largely, with the benefit of class constructors and static calls being similar to Java, and incrementally expose the In Java We use generic type parameter whenever possible. But some APIs need more context which can't be inferred from Another thing to consider is static methods, of which there can be many. My opinion is static methods and constructors should be same in interface they provide - although they are coded differently. In Java we don't have factory methods as language feature, so I use static methods as named constructors very often. There should not be a special status for constructor than static method in the interface we export to user. The entry points to JNI in generated code are: constructors, array constructors and static methods. So if you want to pass any JVM context, allocator or reference pool, you should have near-uniform API for all of these. I think having any methods on
var documents = JniArray(PDDocument.klass)
var documentList = List.of2(PDDocument.klass, myDocument, myOtherDocument);
|
Does this happen even if both |
Yes, as far as I remember, when I tried this, I put both |
Let’s consider the following Java Class: public class MyStack<T> {
private Stack<T> stack;
public MyStack() {
stack = new Stack<>();
}
public void push(T item) {
stack.push(item);
}
public T pop() {
return stack.pop();
}
} Currently this generates the following Dart class without any of the methods: class MyStack<T extends jni.JObject> extends jni.JObject {
MyStack.fromRef(ffi.Pointer<ffi.Void> ref) : super.fromRef(ref);
/// The type which includes information such as the signature of this class.
static jni.JObjType<MyStack<T>> type<T extends jni.JObject>(
jni.JObjType<T> $T,
) {
return _$MyStackType(
$T,
);
}
static final _ctor =
jniLookup<ffi.NativeFunction<jni.JniResult Function()>>("MyStack__ctor")
.asFunction<jni.JniResult Function()>();
/// from: public void <init>()
MyStack() : super.fromRef(_ctor().object);
}
class _$MyStackType<T extends jni.JObject> extends jni.JObjType<MyStack<T>> {
final jni.JObjType<T> $T;
const _$MyStackType(
this.$T,
);
@override
String get signature => r"Lcom/github/dart_lang/jnigen/generics/MyStack;";
@override
MyStack<T> fromRef(jni.JObjectPtr ref) => MyStack.fromRef(ref);
}
extension $MyStackArray<T extends jni.JObject> on jni.JArray<MyStack<T>> {
MyStack<T> operator [](int index) {
return MyStack.fromRef(elementAt(index, jni.JniCallType.objectType).object);
}
void operator []=(int index, MyStack<T> value) {
(this as jni.JArray<jni.JObject>)[index] = value;
}
} Let’s try adding the pop method, for simplicity assume final jni.JObjType<T> $T;
/// from: public void <init>()
MyStack(this.$T) : super.fromRef(_ctor().object); Now MyStack.fromRef(this.$T, ffi.Pointer<ffi.Void> ref) : super.fromRef(ref); And with that, this will work: T pop() {
return $T.fromRef(_popFromC());
} But now the arrays will have a problem with their MyStack<T> operator [](int index) {
return MyStack.fromRef(elementAt(index, jni.JniCallType.objectType).object);
} So now we need to know the MyStack<T> operator [](int index) {
return MyStack.fromRef(($E as _$MyStackType<T>).$T,
elementAt(index, jni.JniCallType.objectType).object);
} Now for some static jni.JArray<jni.JInt> getArr() =>
jni.JArray<jni.JInt>.fromRef(jni.JInt.type, _getArr().object);
Although this code is generated, we have a lot of duplicated logic:
Moving to a solution where class act as the both the class and the type, for example by throwing if we're calling an instance method on an "unconstructed" object, would make both the codegen and the generated code much cleaner. As we don't have to have a copy of every type param in both places. // using .call() instead of ..build() to show another option
final myStack = MyStack(T())(); |
That would indeed only work if we conflate the two concepts into a single class. If we don't conflate them but only make the Type objects more central, we would still always copy the type type objects into the normal objects because methods need to access them. final type = MyStack(T());
final object = type();
// object.push(...)
final object2 = object.pop(); // uses the saved type in in object. So we have kind of three options then if I understand it correctly:
Am I understanding you correctly, that you're no longer advocating for option 2 (your first post), but option 3? And we're trying to solve two related problems here I believe: A. Having the actual information to be able to invoke the right things in Java and Dart. (We need access to type objects) A is a hard requirement. For B we'd need to write multiple examples to see what the code looks like. (Your post above only writes 2 lines of example, which is too not enough for a decision.) All three API designs can satisfy A.
Shouldn't we only have a field in the type class, and forward the relevant things from the object?
Side note: I'd hope |
Closing in favor of #705. Since the named argument approach allows us to have type inference. |
Current state of things
Assuming that dart-archive/jnigen#118 gets merged, arrays are instantiated this way (See #720):
jnigen
generated classes are instantiated this way:Instantiating an object from a class that is not generated by
jnigen
is done this way:These feel inconsistent.
Using TypeClasses for everything!
Consistency
Creating arrays would be similar:
jnigen
generated classes will be instantiated this way:For the classes that have not been generated, we could create a typeclass:
Creating custom classes that were not generated
The nice thing is that we could customize the class and use some other class instead of JniObject:
Generics
We could also support generics (See #759):
So we can do
Static methods
Static methods could also be called from the instances of TypeClasses instead of the classes themselves. This is very similar to the object creation.
Passing context to support multiple JVMs and more
All this also allows more context to be added to the object creation, for example the JVM on which this object should be created on (See #722).
Some ways to do this:
newInstance
methodsnewInstance
methods returning a function (Obj Function(JniContext)
) instead of the object directlyDrawbacks?
The main drawback is that the syntax becomes less similar to Dart object creation. As
SomeClass(...)
becomesSomeTypeClass.newInstance(...)
.The text was updated successfully, but these errors were encountered: