google / jni-bind

JNI Bind is a set of advanced syntactic sugar for writing efficient correct JNI Code in C++17 (and up).

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Why jni-bind requires to specify default (parameterless) constructor if you specify your custom constructor?

Antonz0 opened this issue · comments

commented

Hi.

I don't get it, looks that if I specify my custom constructor, then jni-bind requires to also declare parameter-less constructor, and documentation says so:

If you omit a constructor the default constructor is called, but if any are specified you must explicitly define a no argument constructor.

    static constexpr jni::Class kClass
    {
        "some/class/name",
        jni::Constructor {jstring{}, jint{}, jboolean{}},
        jni::Constructor {}, // <=== Doesn't compile without that. 
    };

But why? What if my class doesn't have a parameterless constructor in Java?

Perhaps I worded the documentation poorly, however, I'm actually trying to prevent exactly what your question is prodding at.

If the second constructor (with no arguments was omitted), then LocalObject<kClass>{} would not compile. In other words, if you specify any constructor, you must also explicitly provide a zero argument constructor (if one indeed is present).

commented

But I don't get it - rules for Java regarding default constructors are the same as in C++: it is generated only if no explicit constructor is specified. And if an explicit constructor is declared, there is no default ctor, therefore it is invalid to try to construct an instance of such class from JNI.
This means that we are interested that LocalObject<kClass>{} will not compile. Why do we need it for such classes?
If I want to construct such an object, I will do it with a constructor with parameters and then pass it via std::move to where it is needed.

To the first sentence, " ... it [the constructor] is generated only if no explicit constructor is specified.". This documentation's counterpart is likely this. Consider the block below:

struct A {};  // default ctor

struct B {
 public: 
   B() {}     // explicit ctor, no args
};

struct C {
  explicit C(int a) {}    // explicit ctor, 1 arg (default is *not* present).
};

struct D {
  explicit D() {}         // explicit ctor, no args
  explicit D(int a) {}    //  explicit ctor, 1 arg
};

int main(int, char* ars[]) {
  A a {};
  B b {};
  // C c1 {};   // doesn't compile (explicit constructor present).
  C c2 {123};
  D d1 {};      // explicitly specified, permitted to compile.
  D d2 {123};

  printf("%p, %p, %p, %p, %p\n", &a, &b, &c2, &d1, &d2); // for warnings
}

JNI Bind follows the pattern above. Apologies, I'm not sure I fully understand the latter half of your question, however, I guess it's worth pointing out that a class very well might have two equivalent and valid ctors, one with args, one without. You may not want to pass args to construct the class. Any subsequent moves are not really relevant to how the object was built.

I hope that helps?

commented

I mean that if I have a Java class like this:

package com.some.package;

class SomeJavaClass
{
    SomeJavaClass(int someArg)
    {
       ...
    }
}

Then this JNI-Bind counterpart is wrong:

    static constexpr jni::Class kClassSomeJavaClass
    {
        "com.some.package.SomeJavaClass",
        jni::Constructor {jint{}},
        jni::Constructor {},  // <==== THIS IS WRONG, Java type doesn't have a default constructor.
    };

because it may allow creating default-constructed Java objects of such class, which doesn't makes sense. But JNI-Bind currently enforces this requirement and C++ code will not compile if I don't add this constructor:

    // NOT COMPILING.
    static constexpr jni::Class kClassSomeJavaClass
    {
        "com.some.package.SomeJavaClass",
        jni::Constructor {jint{}},
    };

You are stating that this requirement was added to allow LocalObject<kClassSomeJavaClass>{} to compile. But why it must compile for such classes, what we will be constructing then? Such construct should be disabled, causing a compilation error.

If I need the instance of the class and then I want to pass it (to Java array wrapper, or as argument) I will construct it like this and use the std::move() :

    jni::LocalArray<jobject, 1, kClassSomeJavaClass> someJavaArray {length, jni::LocalObject<kClassSomeJavaClass>{}};

    for (size_t i = 0; i < length; i++)
    {
        jni::LocalObject<kClassSomeJavaClass> someJavaClassInstance { i };
        someJavaArray.Set(i, std::move(someJavaClassInstance ));
    }

Ohhh, sorry, perhaps that is the fundamental confusion. You are correct, it makes no sense, and you can just remove the explicit empty Constructor listed. If you try to compile objects with an empty constructor, it will fail (which is what you correctly want).

I'm not sure why the above wouldn't compile, it is explicitly tested here ( and I just double checked the CTAD style as you are using is correct). Are you possibly inferring the breakage from the documentation?

The code snippet you provided above looks correct to my eye, you should be able to just delete Constructor{} from your class definition and it should just work. If not, could you provide a Godbolt sample I could copy?

commented

Sorry too, I should have provided a sample code with a proof and a compiler messages. I'm usually doing this, but the documentation note distracted me and caught my attention. And also, compiler messages (clang from Android NDK r25c) were so huge and cryptic and not pointing to the exact place of the issue (as it frequently happens with heavy-templatized code).

Looks like I've found why it was requiring a default constructor, it was all because I was using an object array, and here's how I declared it:

jni::LocalArray<jobject, 1, kClass> someArray {length, jni::LocalObject<kClass>{}};

Obviously, such an approach is wrong for Java classes without default ctor, because it tries to construct an array filled with the default-constructed object elements, and only after that I was setting real (correctly constructed objects to it):

for (size_t i = 0; i < length; i++)
{
     const auto& someData = someInputDataArray[i];
     jni::LocalObject<kClass> obj { someData.stringField.c_str(), someData.intField, someData.boolField  };
     someArray.Set(i, std::move(obj));
}

Somehow it was working, and not crashing the app, but wrong, anyway.
Here I've found how I can get it working correctly:

jni::LocalArray<jobject, 1, kClass> someArray {length, jni::LocalObject<kClass> {static_cast<jobject>(nullptr)}};

It all ends up calling JNI NewObjectArray() API with a nullptr to the initialElement and it seems to work fine despite JNI documentation lacking details about how valid it is.

So, you can probably close this issue now, maybe you may want to update the documentation a bit about constructors if you feel that it can be improved. Maybe adding an example with supplying nullptr as a default value for an array element can be also helpful - it's up to you. Overall I'm very happy with JNI-Bind, it's much more convenient than jni-hpp which I used before.

P.S. Offtopic: documentation about arrays also seems to be lagging a bit? It misses the "rank" template argument, which is now required at least for object arrays with kClass specification. It took me also some time digging into the code to find it.

Ah! Yes, that would certainly do it. Actually, I suppose the following syntax should be allowed:

jni::LocalArray<jobject, 1, kClass> someArray {length} ;

The idea being that if no template object is given nullptr is assumed. I'll add this shortly and close the bug.'

Re: documentation, I absolutely agree. Actually, I was trying to land the documentation for classloaders, but I realised I couldn't write it until I fully decided that API was. In doing this, I consolidated the type system (including arrays).

To be honest, I didn't even expect anyone to be using that functionality yet (it's pretty bleeding edge). I'll try to add some documentation soon.

Thanks a lot for your interest in the project!

Alright, ad8c682 should have the constructor you want.

I'll cut a new release soon. Thanks!

commented

Many thanks for that! 👍

P.S. Just wandering - is there a place where I can ask questions about some behavior that is not precisely a bug, and discuss some ideas? Probably "Discussions" should fit this role. I will try this later because I'm experimenting with some stuff and have something to ask already.

I would really appreciate that! Feedback is really helpful to improving the project. Discussions or bugs are both fine, I monitor this GitHub fairly closely.