on usability (was Re: [foreign] RFR 8211060: Library.getDefault() does not work on Windows)

Maurizio Cimadamore maurizio.cimadamore at oracle.com
Wed Sep 26 11:05:31 UTC 2018


Jorn actually made most of my points, but I will try to add something in 
here



On 26/09/18 02:14, Samuel Audet wrote:
> We can do a lot of wrapper magic in either Java or C++. JavaCPP 
> already does for JNI what Jorn is describing we could do for Panama:
> https://github.com/bytedeco/javacpp-presets/tree/master/systems#the-srcmainjavatestavxjava-source-file 
>
>
> If we consider JNI to be "legacy",
It's not that JNI is legacy - but JNI performances (and safety, and 
usability) cannot be improved much. If you want to get performances that 
are similar to those you'd get with a native compiler, using JNI is just 
not the tool for the job. Existing solutions such as JavaCPP or JNR can 
take the usability pain out of JNI and offer a smoother experience. But, 
being based on JNI, they can't improve over what JNI has to offer, at 
least not performance-wise. For that you need to reach out for some 
trick under the hood (either in Hotspot or in Graal).

> it makes a lot of sense to try and do some acrobatics like that to 
> support legacy systems, but why not make the new hotness actually 
> _usable_? Take a look at how Swift does it:
> https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/using_imported_c_structs_and_unions_in_swift 
>
Now, I believe in Swift, when you interop with native code you really 
"import" that native code - meaning that some definition will be 
generated from the corresponding C/Objective-C header. If you squint, 
this is not too different from what we do with jextract, is it? After 
the importing process is done, you can use the struct as if it were a 
Swift struct; in Panama you can use it as an interface. There are some 
syntactic differences of course - the fact that Swift has properties 
allows it to model struct access (get/set) as field access, in Panama we 
have to do with interface calls.

Btw, on this topic of accessors, native structs really need 3 accessors: 
get/set/addressof - I don't think even Swift is powerful enough to do 
the 3rd, which means for that you are back to a regular method call?

Anyway, I don't see a huge usability gap here other than the one imposed 
by different language restrictions.
>
> Now that's what I call *usable*. Panama is very far from that level of 
> usability. And it's not because the Java language is somehow 
> handicapped. Check what the guys over at GraalVM are doing:
> https://github.com/oracle/graal/blob/master/substratevm/src/com.oracle.svm.tutorial/src/com/oracle/svm/tutorial/CInterfaceTutorial.java 
>
Well, if you read most of the code you would actually have trouble 
telling whether it's Panama or Graal. The main concepts are there - 
there is a way to speak about pointers, structs are modeled as 
interfaces, ...

The only real difference is native methods (and even that is a very 
small one) - Graal allows you to model native calls as calls to 'Java 
native' methods which have some annotations; if you AOT everything (with 
substrateVM/native image) the AOT compiler can compile the call away 
using a direct native call. You get better performances (no JNI), you 
lose flexibility (dynamic loading). I'd say the Graal solution is in 
spirit very similar to the one we provide here, with the difference that 
Panama does the magic at runtime (JIT) as opposed to AOT.

So, to summarize, the biggest difference I see (apart from naming 
differences for e.g. pointer types etc.) is that Graal/Native Image 
support uses 'annotated' 'native' methods. Panama uses 'annotated' 
'abstract' (interface) methods. Oh - there's also another difference - 
Panama does not attempt to model enums as Java enums as that's not 
complete, but that's a different story. Again, not much difference to 
see here?

Maybe you refer to the fact that the annotations in the Graal example 
are easier to read than Panama's (which you didn't say, but I could 
agree); in that case please do keep in mind that Panama metadata is 
designed to be language neutral so as to maximize applicability and not 
to bake any language assumptions (e.g. pointers are 64bit) into the VM. 
If we're only talking C/C++ this might sound odd, but it pays off in 
that you can also naturally model things that do not come from C (e.g. 
protobuf/flatbuffer schemas).
>
> It also features performance that's already apparently higher than JNI:
> https://cornerwings.github.io/2018/07/graal-native-methods/
>
> Why not do something user friendly like that in Panama's case as well? 
> What's the rationale to make it all in the end as complicated to use 
> as JNI? Maybe there's something I'm missing, so please point it out.
I believed I showed in the JMH thread how linkToNative is similarly 
capable of sailing well past of JNI. And I have reasons to believe a 
similar optimization as the one done in C2 for linkToNative can be done 
with the Graal compiler.

So, all things consider I (still) fail to see how the design choices we 
have made are precluding us certain usability goals, or certain 
performance targets  - which is what you seem to suggest.

Maurizio
>
> Samuel
>
>
> On 09/24/2018 10:37 PM, Maurizio Cimadamore wrote:
>> Having a pre-extracted stdlib bundle is something we have considered 
>> - quoting from [1]:
>>
>> "Now, since most (all?) of the libraries out there are going to assume
>> the availability of some 'standard library', let's also assume that some
>> extracted artifact for such library is available and that jextract
>> always knows how to find it - this is the equivalent of java.base for
>> the module system, or java.lang for the Java import system. This
>> addresses the bootstrapping issue."
>>
>> In time we'll get there, I don't see any real technical obstacles to 
>> get to your 'optimal' snippet.
>>
>> I think there are two aspects that I'd like to draw attention upon:
>>
>> 1) Magic does not come for free. E.g. it might "seem" that JNI has a 
>> more direct approach to calling native methods (ease of use issues 
>> aside). In reality it's just that the complexity of calling that 
>> native method, marshalling arguments, unmarshalling returns, dealing 
>> with native thread transitions and what's not has just been pushed 
>> under the JVM rug. So, yes, you can "just" call getpid - but the 
>> burden is on the VM. Now, the JNI support is already quite complex - 
>> I can't honestly imagine a sane way for the VM to support any given 
>> invocation scheme that a user might wish to see supported. This is 
>> why Panama is betting on Java code + layouts to do the lifting: that 
>> way the VM interface can be kept simple (which has significant 
>> payoffs - as the resulting code can be optimized much more - see the 
>> linkToNative experimental results in [2]).
>>
>> 2) As your example points out, while calling 'getpid' is something 
>> that seems 'easy' enough - 'puts' is already some other beast. It 
>> takes a pointer to some memory location where the string is stored. 
>> The JNI approach is to pass the Java string as is, and then do the 
>> wiring in native code. That is, there's no free lunch here - either 
>> you do the adaptation in Java, or you do it in native code (**). 
>> Panama gives you a rich enough API to do all such adaptations in 
>> Java, so that all native calls are... just native calls (again this 
>> means more regularity which means more performances). Having opaque 
>> native code snippets which do argument adaptation is not very optimal 
>> (and optimizable) for the JVM. With Panama you can create a 
>> good-looking API which internally uses pointers/scopes and delegates 
>> to the right native method - all done in Java. On top of that, our 
>> plans cover a so called 'civilization' layer (see [3]), by which 
>> users will be able to customize what comes out of jextract in order 
>> e.g. to tell that for 'puts' they really want a Java String argument 
>> and not a Pointer<Byte>; again this will be done in a more general 
>> way, so that the binder will be pointed at a pair of functions which 
>> can be used to map the user provided data to and from native code.
>>
>> (**) for an example of how interfacing with standard libraries needs 
>> some kind of wrapping, even in JNI - look at [4]; this file is 
>> essentially a collection of system calls which are wrapped by some 
>> logic (e.g. to check errno, ...). I claim that there is something 
>> _fundamentally_ wrong with code like this, in that the native code is 
>> mixing two concerns: (i) performing the required native call and (ii) 
>> adjusting input/output/errors of the call in a way that is suitable 
>> to the corresponding Java API. Why shouldn't the Java API itself be 
>> in charge of doing (ii) ?
>>
>> Maurizio
>>
>> [1] - 
>> http://mail.openjdk.java.net/pipermail/panama-dev/2018-August/002560.html
>> [2] - 
>> http://mail.openjdk.java.net/pipermail/panama-dev/2018-September/002652.html 
>>
>> [3] - 
>> http://mail.openjdk.java.net/pipermail/panama-dev/2018-April/001537.html
>> [4] - 
>> http://hg.openjdk.java.net/jdk/jdk/file/tip/src/java.base/unix/native/libnio/fs/UnixNativeDispatcher.c#l314 
>>
>>
>>
>>
>>
>> On 24/09/18 13:34, Jorn Vernee wrote:
>>> I agree with the usability point. In C++ it's as simple to call puts 
>>> as doing:
>>>
>>>     #include <stdio.h>
>>>
>>>     int main() {
>>>         puts("Hello World!");
>>>     }
>>>
>>> And I think the optimal Java equivalent would be something like:
>>>
>>>     import static org.openjdk.stdio.*;
>>>
>>>     public class Main {
>>>
>>>         public static void main(String[] args) {
>>>             puts("Hello World!");
>>>         }
>>>
>>>     }
>>>
>>> This can be facilitated by creating a 'singleton facade' for the 
>>> library interface like so:
>>>
>>>     public class stdio {
>>>
>>>         private static final stdioImpl lib = 
>>> Libraries.bind(lookup(), stdioImpl.class);
>>>
>>>         public static int puts (String message) {
>>>             try(Scope scope = Scope.newNativeScope()) {
>>>                 Pointer<Byte> msg = scope.toCString(message);
>>>                 return lib.puts(msg);
>>>             }
>>>         }
>>>
>>>         ...
>>>     }
>>>
>>> Such a facade class could be shipped with the JDK or perhaps as an 
>>> artifact on maven central, or maybe it could be an additional output 
>>> of jextract.
>>>
>>> But there is only so much you can do automagically from the Java 
>>> side. When working from C/C++ you have the compiler filling in the 
>>> blanks. For instance, it automatically allocates storage for string 
>>> literals. Java does that as well for Java string literals `String s 
>>> = "Hello";`, but it can not do that for native strings, and you have 
>>> to use the Scope API to do that manually. In some cases, like the 
>>> above, you can write glue-code to make that automatic, but I think 
>>> at some point things become too complex for that, and there will 
>>> always be some usability barrier to interop.
>>>
>>> Jorn
>>>
>>> Samuel Audet schreef op 2018-09-24 13:38:
>>>> FWIW, I think the factory method pattern should be reconsidered
>>>> entirely. In C/C++, when we want to call say getpid(), we don't start
>>>> loading stuff up before calling getpid(), we call getpid()! Why not do
>>>> the same from Java? From a usability point of view, not loading stuff
>>>> manually works fine for JavaCPP...
>>>>
>>>> Now, I know you're going to start taking about interfaces and what
>>>> not. You said that you had plans to introduce an entirely new array
>>>> type just to make it more friendly with vector instructions and native
>>>> libraries. Why not start thinking about an "interface" that would be
>>>> friendly to native libraries as well? Why stop at arrays?
>>>>
>>>> Samuel
>>>>



More information about the panama-dev mailing list