-
-
Notifications
You must be signed in to change notification settings - Fork 51
Adding JProxy, a wrapper that allows Java-like syntax for field and method access #91
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
Conversation
created init_current_vm function
Adding new conversions to convert.jl
Thanks Bill, this is amazing. I want to spend some time thinking about the interface. is But either ways, this kind of functionality is something I've wanted for a long time. Thanks for this, its very useful. |
Thanks! It would be possible to make JProxy the main API, yes, but I didn't do that because I didn't want to make that level of decision for your project :). It would be possible to merge JProxy's functionality into JavaObject and remove the JProxy type. JProxy currently uses an "interpretive" approach, which is good enough for a first cut, I think. A next step might be to define a function for each method when a class is first discovered. JMethod proxy could become a value type parameterized on the method name and defining class, so the Julia function for a method could be something like this: function (pxy::JMethodProxy{Symbol("java.util.ArrayList"), :remove})(i::int32)
...
end This would remove the need for argument type conversion and also let Julia do the method dispatch instead of using reduce with a "generality" function, like I'm doing right now. It would also remove decision making for the result since the type is known at compile time. To fit into the Julia model more idiomatically, it could also define an explicitly named method. Then both |
Thinking about it, I think the class parameter I proposed for JMethodProxy can't be just a symbol, it would have to be a type itself and the system would have to generate a type hierarchy parallel to Java's class hierarchy. I'm going to start working on the compiled version of JProxy in another branch. |
I would be up for that.
Thanks for running with this. paging @dfdx and @ExpandingMan for their thoughts? |
Sure thing, grafting things onto other things is my bag, man. Right now I'm focusing on code cleanup and using compilation instead of dynamic techniques for methods. After that I'll look at merging functionality into JavaObject and removing the JProxy type. |
Awesome, thanks so much for this! It's amusing that we'll have this ability here before PyCall as that package is much more widely used. I'm all for making I'm definitely not a Java expert, I got involved in this package mostly because I needed Anyway, thanks again! I'm sure having this will be a huge relief for those working on packages like JDBC.jl and Spark.jl. |
We should be very careful not to run into method generation problem in Julia. Say, if we have:
It may be quite troublesome if you already have
Later (implicit) definition would hide exported one, leading to hard-to-debug issues. Some possible fixes:
I also hope that we retain old API based on |
This is a good point, mirroring parts of an entire language and SDK will almost guarantee naming conflicts . I'll just generate proxy-style functions only and not named ones. That will guarantee no naming conflicts. function (pxy::JMethodProxy{Symbol("wait"), <:java_lang_Object})(a1::Int64)
_jcall(getfield(pxy, :obj), (methodsById[1]).id, C_NULL, Int32, (Int64,), a1)
end
|
array arg conversion did not work for empty arrays adding JConstructor
I have the compiled version of the proxy mostly working here. I still have to do array conversion. Here are some example generated methods: function (pxy::JMethodProxy{Symbol("contains"), <:java_util_AbstractCollection})(a1::Union{Number, String, JProxy})
_jcall(getfield(pxy, :obj), (methodsById[30]).id, C_NULL, UInt8, (JavaObject{Symbol("java.lang.Object")},), box(a1)) != 0
end
function (pxy::JMethodProxy{Symbol("add"), <:java_util_AbstractList})(a1::Number, a2::Union{Number, String, JProxy})
_jcall(getfield(pxy, :obj), (methodsById[44]).id, C_NULL, Int32, (Int32, JavaObject{Symbol("java.lang.Object")}), (Int32)(a1), box(a2))
end
function (pxy::JMethodProxy{Symbol("get"), <:java_util_ArrayList})(a1::Number)
asJulia(JavaObject{Symbol("java.lang.Object")}, _jcall(getfield(pxy, :obj), (methodsById[67]).id, C_NULL, Any, (Int32,), (Int32)(a1)))
end
function (pxy::JMethodProxy{Symbol("get"), <:java_util_AbstractList})(a1::Number)
asJulia(JavaObject{Symbol("java.lang.Object")}, _jcall(getfield(pxy, :obj), (methodsById[47]).id, C_NULL, Any, (Int32,), (Int32)(a1)))
end I'm generating a parallel type hierarchy in the JavaCall module with names like A few things:
|
I'm still working on this -- done a few overhauls. Lately I realized that JavaObjects use LocalRefs and JProxy really needs to use GlobalRefs and it looks like that means JProxy should use Ptr{Nothing} instead of JavaObject. So JProxy would be an alternative interface instead of a wrapper but there would still be a simple way to convert between JProxy and JavaObject, like |
Sorry this is taking so long -- dealing with a ton of RL interrupts. I just committed a bunch of changes to my branch in my repo: https://github.com/zot/JavaCall.jl/tree/jproxy-compiled I'll keep you guys informed about progress. |
end | ||
|
||
function genMethods(class, gen, info) | ||
methodList = listmethods(class) |
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.
unused?
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.
gen.jl is unused right now -- it's a collection of the code generation stuff I had in there before. I realized I'd better get the dynamic version running first before doing code generation.
end | ||
end | ||
|
||
classfortype(t::Type{JavaObject{T}}) where T = classforname(string(T)) |
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.
May T be an array in java?
If so, we should fix classforname to work with something like "[Ljava.lang.String;"
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.
classforname does work with "[Ljava.lang.String;":
JProxy(classforname("[Ljava.lang.String;")).getName()
returns
"[Ljava.lang.String;"
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'm planning to remove dependencies on JavaObject from proxy.jl but I wanted to get it working first.
@Class(java.util.ArrayList) creates a static proxy you can use as a constructor
@zot really looking forward to using this - thanks for all the work you've put in. One question - and forgive me if this is obvious - but why does this functionality need to be exposed via the JProxy wrapper? Is there a major reason why the dot syntax could not apply directly to the JObjects themselves? It seems like once this merges most use-cases would be easier with JProxy, so every constructor call will end up looking like |
No problem! My main motivation is that I spend most of my day working in Java and I want to use Julia as a beefed up developer console for our product :). I prefer Julia to Java so this is one way I can get to use it for work. Also, I like connecting things to other things.
Three reasons:
To create instances now, btw, you can use the You can also use them to access static members, like Looking at the name though, I'm thinking now that |
OK, tests work fine on one of my machines which has OpenJDK 1.8.0_191-8u191-b12-0ubuntu0.18.10.1-b12 but fail on the machine that has Oracle's JDK 1.8.0_144 in a way similar to the Travis failure. Testing with that now... |
Have had some RL interrupts here -- heading for a 2-week trip though during which I plan to try to get the tests working on both machines. I don't think I'll have much Internet access though... |
I updated to JDK 1.8.0_191 and the tests work on the machine that was failing earlier. It seems to be the same problem the Travis box is having. |
I got slammed with obligations and I don't see a lot of spare time in the near future. This is quite functional but I think it still needs a little more love. I'd greatly appreciate it if someone would be willing to jump in and help out here... |
I hoped to take a look at it on the weekend, but it seems like another project will take me busy for some time. Let's keep it open for now - this PR won't be lost anyway, so we can just wait for someone with related project to get to it (e.g. on my next iteration with Spark.jl). |
Just stumbled upon JavaCall.jl and looked for a bit more convenient way to call java than using jcall. Then I found this and it looks quite nice, what is the status? Is there currently any alternative to jcall? |
I haven't worked on it for quite a while -- I was hoping someone might step up to help out with it... |
I have resolved conflicts on this PR at #112. Tests are passing there including test added for |
Thanks @mkitti for picking this up. This is a lot of code, a pretty major re-write of of this package, and I was not sure of having the time and effort to maintain this without the help of the original author of these changes, even though I've long wanted this functionality. Hopefully with Mark's help we can take this to it's conclusion. |
Yeah, I know it's a ton of stuff, sorry about that -- rather than taking an "interpretive approach" and just delegating arguments, I took a "compiled approach" and generated methods with typed parameters in an effort to allow better code generation and error detection. Also, I'm not a super experienced Julia programmer so I'm not positive my techniques are the best but I figure something is better than nothing :) Although I don't have time at this point to actually be responsible for maintaining the code, I'm still more than happy to help! |
The direction we should take on this is to deploy this as a distinct package in a subdirectory of this repository that depends on core JavaCall. The task now is to figure out a minimal patch that needs to be made to JavaCall itself and what can be put into an accessory package. |
That sounds like a good idea to me, I was never intending to replace JavaCall |
@@ -129,7 +183,9 @@ function jnew(T::Symbol, argtypes::Tuple, args...) | |||
if jmethodId == C_NULL | |||
throw(JavaCallError("No constructor for $T with signature $sig")) | |||
end | |||
return _jcall(metaclass(T), jmethodId, jnifunc.NewObjectA, JavaObject{T}, argtypes, args...) | |||
result = _jcall(metaclass(T), jmethodId, jnifunc.NewObjectA, JavaObject{T}, argtypes, args...) | |||
deletelocals() |
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.
@zot Why is deletelocals()
necessary? Is the garbage collector not working for the local references for some reason?
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.
Localrefs are raw pointers / handles into the JVM so the Julia gc does not interact with them and we're not holding onto them with a finalizable object, just a Ptr{nothing} (the Julia objects in JavaCall now use global refs not local refs).
I think that local references created outside a call will not be collected until you explicitly deallocate them. It's possible, however, that the JVM blows away all localrefs at the end of each call so I'm not positive about this.
I think if every JNI call is guaranteed to blow away all local refs, then we don't have to track them.
I do remember that Java objects sometimes mysteriously changed identity until I changed it to use global refs so maybe it does blow away all localrefs at the end of each call into the JNI and we don't need to track them.
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.
Thank you, @zot . Sorry for being a bit curt. It was getting late.
I've quoted the documentation below.
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#referencing_java_objects
In most cases, the programmer should rely on the VM to free all local references after the native method returns. However, there are times when the programmer should explicitly free a local reference. Consider, for example, the following situations:
- A native method accesses a large Java object, thereby creating a local reference to the Java object. The native method then performs additional computation before returning to the caller. The local reference to the large Java object will prevent the object from being garbage collected, even if the object is no longer used in the remainder of the computation.
- A native method creates a large number of local references, although not all of them are used at the same time. Since the VM needs a certain amount of space to keep track of a local reference, creating too many local references may cause the system to run out of memory. For example, a native method loops through a large array of objects, retrieves the elements as local references, and operates on one element at each iteration. After each iteration, the programmer no longer needs the local reference to the array element.
I'm not clear if we are necessarily in one of those special cases. I suppose we should figure how to check if the JVM is doing that job in this context. I'm not clear what would constitute a native method here.
If this is needed, I'm wondering if we should just build it into _jcall
. Also, is this not needed for methods that return primitives?
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.
Do we also need to track local references in the other _jcall
:
Line 289 in e7e8700
return convert_result(rettype, result) |
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 I did build it into _jcall, or maybe I'm misunderstanding you?
Right, primitives don't need to be registered but registerlocal() checks for references before adding it to the list.
Looks like I added the registerlocal() function, all it does is add references to an array so they can be unallocated later.
I'm not sure whether or not _jfield needs this.
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.
No problem. My experiments with this were in 2018 and my memory of them is a bit hazy. If I remember something other than the things I recounted already I'll mention it.
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.
We should consider using PushLocalFrame
and PopLocalFrame
to deal with local references rather than implementing our own management system. I am not sure if we need a global reference for everything.
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#PushLocalFrame
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.
Sure, whatever works. Global references are really only needed for the proxies
We do need to ensure that references returned from the JNI don't get randomly collected before the Julia code has a chance to decide what to do with it so think deleting locals inside of jcall
would be dangerous.
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 agree. The Java garbage collector does not seem to functioning for JavaCall code because we are not using PushLocalFrame
and PopLocalFrame
. Without those, the Java GC does not know when we are out of the local frame.
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.
If we release the returned reference inside of jcall
, Java can GC the return value before Julia has a chance to use it, so we need to manage returned references outside of jcall. Otherwise they won't be safe to use for the code that uses jcall.
It might make sense to provide a function that takes a do block and runs it in between a push and a pop so you can safely run a bunch of jcalls in the block.
Like:
java() do
....
end
Could make jcall throw an error if there's no pushed frame so you're essentially required to use java() (or push a frame yourself)
This lets you say things like:
Note that a.clone() returns a JProxy, so you can use fields and methods on objects from fields and methods.