Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged
merged 40 commits into from
Nov 2, 2020

Conversation

zot
Copy link
Contributor

@zot zot commented Sep 14, 2018

This lets you say things like:

julia> a=JProxy(@jimport(java.util.ArrayList)(()))
[]

julia> a.size()
0

julia> a.add("hello")
true

julia> a.get(0)
"hello"

julia> a.isEmpty()
false

julia> a.toString()
"[hello]"

julia> b = a.clone()
[hello]

julia> b.add("derp")
true

julia> a == b
false

julia> b == b
true

julia> JProxy(@jimport(java.lang.System)).getName()
"java.lang.System"

julia> JProxy(@jimport(java.lang.System);static=true).out.println("hello")
hello

Note that a.clone() returns a JProxy, so you can use fields and methods on objects from fields and methods.

@aviks
Copy link
Collaborator

aviks commented Sep 15, 2018

Thanks Bill, this is amazing.

I want to spend some time thinking about the interface. is JProxy(@jimport(..... the right abstraction. Or rather, is @jimport the best way to begin with. A few years ago it seemed like the most efficient interface, but I want to take this opportunity to re-think that.

But either ways, this kind of functionality is something I've wanted for a long time. Thanks for this, its very useful.

@zot
Copy link
Contributor Author

zot commented Sep 15, 2018

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 remove(arrayList, 1) or arrayList.remove(1) would work.

@zot
Copy link
Contributor Author

zot commented Sep 15, 2018

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.

@aviks
Copy link
Collaborator

aviks commented Sep 15, 2018

It would be possible to merge JProxy's functionality into JavaObject

I would be up for that.

I'm going to start working on the compiled version of JProxy in another branch.

Thanks for running with this.

paging @dfdx and @ExpandingMan for their thoughts?

@zot
Copy link
Contributor Author

zot commented Sep 15, 2018

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.

@ExpandingMan
Copy link
Contributor

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 getproperty the main interface for all JavaObjects, I can't see any advantage of having JProxy as a separate object (though thank you for the consideration that went into making that choice). That would of course be a big change and we'd have to be really careful as many tests would probably have to be rewritten.

I'm definitely not a Java expert, I got involved in this package mostly because I needed JDBC.jl to be working reliably, so I certainly don't have any useful insight to how this will all wind up working. I suppose the way to go would be to keep jcall as a "low-level" interface but have it so that most of the time users only need @jimport and getproperty.

Anyway, thanks again! I'm sure having this will be a huge relief for those working on packages like JDBC.jl and Spark.jl.

@dfdx
Copy link
Collaborator

dfdx commented Sep 15, 2018

A next step might be to define a function for each method when a class is first discovered.

We should be very careful not to run into method generation problem in Julia. Say, if we have:

function foo()
    # first import of JSomeObject, defines its methods
    jobj = JProxy(@jimport JSomeObject)
    jobj.bar()
end

bar() may be not defined in the generation that foo() is running in. I'm also not sure there will be no method definition conflicts.

To fit into the Julia model more idiomatically, it could also define an explicitly named method. Then both remove(arrayList, 1) or arrayList.remove(1) would work.

It may be quite troublesome if you already have remove in scope. Say, you have:

using A     # export function `remove()`

# implicitly generated from @jimport
function remove(arr::JArrayList, i::Int32)
    ...
end

Later (implicit) definition would hide exported one, leading to hard-to-debug issues.

Some possible fixes:

  1. Define something like j_remove(arr::JArrayList, i::Int32) to decrease probability of name collision. Note, however, that Java and Julia have different naming convention, so neither doSomeStuff, nor j_doSomeStuff follow Julia conventions which would name it do_some_stuff.
  2. Don't mess with method definition and just keep cache of (static_flag, method_name, arg_types) => method_instance pairs inside of jimported class object.

I also hope that we retain old API based on jcall as well - not only a lot of code have already been written using it, but also its much easier to debug: while working on Spark.jl I found enough JNI weirdness to appreciate ability to work with the most low-level API I can get.

@zot
Copy link
Contributor Author

zot commented Sep 16, 2018

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

A next step might be to define a function for each method when a class is first discovered.

We should be very careful not to run into method generation problem in Julia. Say, if we have:

function foo()
    # first import of JSomeObject, defines its methods
    jobj = JProxy(@jimport JSomeObject)
    jobj.bar()
end

bar() may be not defined in the generation that foo() is running in. I'm also not sure there will be no method definition conflicts.

To fit into the Julia model more idiomatically, it could also define an explicitly named method. Then both remove(arrayList, 1) or arrayList.remove(1) would work.

It may be quite troublesome if you already have remove in scope. Say, you have:

using A     # export function `remove()`

# implicitly generated from @jimport
function remove(arr::JArrayList, i::Int32)
    ...
end

Later (implicit) definition would hide exported one, leading to hard-to-debug issues.

Some possible fixes:

  1. Define something like j_remove(arr::JArrayList, i::Int32) to decrease probability of name collision. Note, however, that Java and Julia have different naming convention, so neither doSomeStuff, nor j_doSomeStuff follow Julia conventions which would name it do_some_stuff.
  2. Don't mess with method definition and just keep cache of (static_flag, method_name, arg_types) => method_instance pairs inside of jimported class object.

I also hope that we retain old API based on jcall as well - not only a lot of code have already been written using it, but also its much easier to debug: while working on Spark.jl I found enough JNI weirdness to appreciate ability to work with the most low-level API I can get.

array arg conversion did not work for empty arrays
adding JConstructor
@zot
Copy link
Contributor Author

zot commented Sep 17, 2018

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 java_util_AbstractList. As you can see, I'm still using _jcall but _jcall makes some decisions which are known at compile time so I think that can be made more efficient or at least it could call a specialized version of _jcall.

A few things:

  • asJulia() is only used if needed
  • booleans are converted in-line (as in ArrayList.contains())
  • numeric arguments are presented as Number and converted in the body
  • Object arguments are presented as Union{Number, String, JProxy} and converted in the body

@zot
Copy link
Contributor Author

zot commented Sep 26, 2018

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 JProxy(jobj) and JavaObject(pxy)

@zot
Copy link
Contributor Author

zot commented Oct 13, 2018

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused?

Copy link
Contributor Author

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))

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;"

Copy link
Contributor Author

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;"

Copy link
Contributor Author

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
@kcajf
Copy link

kcajf commented Nov 27, 2018

@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 JProxy(@jimport(java.util.ArrayList)(())).

@zot
Copy link
Contributor Author

zot commented Nov 27, 2018

@zot really looking forward to using this - thanks for all the work you've put in.

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.

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 JProxy(@jimport(java.util.ArrayList)(())).

Three reasons:

  1. I didn't want to change the way JavaObject works without permission, if whoever owns this code wants me to merge them, I'm happy to do that.
  2. Right now, JProxy uses JavaObject during initialization but I'm already starting to remove the dependencies, once I'm finished removing those, I could merge JProxy into JavaObject and remove JProxy.
  3. I wanted to keep JavaObject stable during development so I had a baseline to check against. Now that JProxy seems to be mostly working, I don't need a baseline anymore :).

To create instances now, btw, you can use the @class() macro which creates a static proxy that can act like a function. So to make an array list you can say @class(java.util.ArrayList)() or you could make a variable const ArrayList = @class(java.util.ArrayList) and just say ArrayList()

You can also use them to access static members, like @class(java.lang.Integer).MAX_VALUE. You can use a proxy on a class object as a constructor, too, like anArrayList.getClass()() but since it's a proxy on a class object, you can't use it for static members although you can use it for getName() and getDeclaredMethods(), just like in Java.

Looking at the name though, I'm thinking now that @static is a much better name than @class.

@zot
Copy link
Contributor Author

zot commented Dec 1, 2018

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...

@zot
Copy link
Contributor Author

zot commented Dec 3, 2018

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...

@zot
Copy link
Contributor Author

zot commented Dec 4, 2018

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.

@zot
Copy link
Contributor Author

zot commented Jan 25, 2019

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...

@dfdx
Copy link
Collaborator

dfdx commented Feb 2, 2019

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).

@schlichtanders
Copy link

Just stumbled upon JavaCall.jl and looked for a bit more convenient way to call java than using jcall.
Was hoping to find a string macro, like python R and cxx support it.

Then I found this and it looks quite nice, what is the status? Is there currently any alternative to jcall?

@zot
Copy link
Contributor Author

zot commented Apr 11, 2020

I haven't worked on it for quite a while -- I was hoping someone might step up to help out with it...

@mkitti mkitti mentioned this pull request Apr 13, 2020
@mkitti
Copy link
Member

mkitti commented Apr 13, 2020

I have resolved conflicts on this PR at #112. Tests are passing there including test added for JProxy

@aviks
Copy link
Collaborator

aviks commented Apr 15, 2020

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.

@zot
Copy link
Contributor Author

zot commented Apr 17, 2020

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!

@mkitti mkitti self-assigned this Apr 24, 2020
@mkitti
Copy link
Member

mkitti commented Apr 24, 2020

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.

@zot
Copy link
Contributor Author

zot commented Apr 27, 2020

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

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()
Copy link
Member

@mkitti mkitti Apr 28, 2020

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?

Copy link
Contributor Author

@zot zot Apr 28, 2020

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.

Copy link
Member

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?

Copy link
Member

@mkitti mkitti Apr 28, 2020

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:

return convert_result(rettype, result)

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

@zot zot Apr 30, 2020

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants