Repository navigation
Proposal: jdcall and jtypeforclass #93
Copy link
Copy link
Open
Labels
Description
- Now the get class for name is compatible with Array and primitive types
Lines 321 to 325 in d6c3678
| function narrow(obj::JavaObject) | |
| c = jcall(obj,"getClass", @jimport(java.lang.Class), ()) | |
| t = jcall(c, "getName", JString, ()) | |
| return convert(JavaObject{Symbol(t)}, obj) | |
| end |
Lines 163 to 168 in d6c3678
| function classforname(name::String) | |
| thread = jcall(JThread, "currentThread", JThread, ()) | |
| loader = jcall(thread, "getContextClassLoader", JClassLoader, ()) | |
| return jcall(JClass, "forName", JClass, (JString, jboolean, JClassLoader), | |
| name, true, loader) | |
| end |
This method may help
function jtypeforclass(cls::JClass)
isarray(cls) = jcall(cls, "isArray", jboolean, ()) != 0x00
if isarray(cls)
jcomponentcls = jcall(cls, "getComponentType", JClass, ())
return Array{jtypeforclass(jcomponentcls), 1}
end
name = getname(cls)
if name == "void"
Nothing
elseif name == "boolean"
jboolean
elseif name == "char"
jchar
elseif name == "short"
jshort
elseif name == "float"
jfloat
elseif name == "double"
jdouble
elseif name == "int"
jint
elseif name == "long"
jlong
else
JavaObject{Symbol(name)}
end
endand the narrow would be
function narrow(obj::JavaObject)
c = jcall(obj,"getClass", JClass, ())
return convert(jtypeforclass(c), obj)
end
narrow(obj::JavaCall.jprimitive) = obj- And it would be possible to call functions without specific return type and argument type
convertible(javatype::Type, juliatype::Type) = hasmethod(convert, Tuple{Type{javatype}, juliatype})
function findmethod(obj::Union{JavaObject{C}, Type{JavaObject{C}}}, name::AbstractString, args...) where C
allmethods = listmethods(obj, name)
filter(allmethods) do m
params = getparametertypes(m)
if length(params) != length(args)
return false
end
all([convertible(jtypeforclass(c), typeof(a)) for (c, a) in zip(getparametertypes(m), args)])
end
end
function jdcall(obj::Union{JavaObject{C}, Type{JavaObject{C}}}, name::AbstractString, args...) where C
matchmethods = findmethod(obj, name, args...)
if length(matchmethods) == 0
allmethods = listmethods(obj, name)
candidates = join(allmethods, "\n ")
error("no match methods $name for $obj, candidates are:\n $candidates")
elseif length(matchmethods) > 1
candidates = join(matchmethods, "\n ")
error("multiple methods $name for $obj, candidates are:\n $candidates")
end
matchmethod = matchmethods[1]
rettype = jtypeforclass(getreturntype(matchmethod))
argstype = tuple(map(jtypeforclass, getparametertypes(matchmethod))...)
# println("type: $rettype ($argstype)")
return jcall(obj, name, rettype, argstype, args...)
endSince narrow and classforname are both exported, so this may be a breaking change, although I think the behavior of the two methods now is strange when it comes to array and/or jint.
Reactions are currently unavailable
Activity
dfdx commentedon Nov 25, 2018
JVM internals aren't that elegant, sometimes you really, really want to call a very specific method. For example, in Spark.jl I once encountered two methods of the same object with the same list of parameter types, but with different return types (which was a result of tricky inheritance and/or Java/Scala compiler differences). Anyway, the only way to call exact method I wanted was to pass exact list of types to JNI, so it's unlikely we will be able to replace
jcall.Are you concerned with the need to specify argument types? If so, have a look at #91 which provides a more high-level API with Java-like syntax and automatic
jcallgeneration (implemented similar to what you propose here). For example, when it's done, you'd able to write something like:And at the same time it will preserve low-level API of
jcall. Does it fit your needs?clouds56 commentedon Nov 25, 2018
void org.apache.spark.sql.Dataset.show(int)void org.apache.spark.sql.Dataset.show(boolean)they have different parameter list while in JavaCall.jl, you have to specific parameter type
ANSWER: yes, if we meet case like this (multiple match found), the
jdcallwould not choose the method for you, you have to use thejcallexplicitly.jdcall, we could achieve chain call like thisNotice here we do not have a line
@jimport org.apache.spark.sql.RelationalGroupedDataset(and have not used any methods in Spark.jl), we do not have to know the intermediate type as what we could do in scala (see the gist for more information)JProxy, I think it is a wonderful API but not the same direction withjdcallwhich I proposed here. (I'm not familiar with JProxy, correct me if I'm wrong)JProxywould automatically wrap all methods usinggenMethods, which may take some time compiling for complex class.JProxywould also encounter the problem when multiple methods in java matches (I don't know how it handle the case), and even worse, it may have conflict in julia side, may not?jdcallwould be faster and has much more small memory footprint if only one method of the class would be called.jdcallis clean, it is just a variant/extension ofjcalljcallapi.JProxyin binding package while usejdcall/jcallin prototype.clouds56 commentedon Nov 25, 2018
TL; DR
JProxyis heavy whilejdcallis light.JProxyprovides a completely new way to interact with java objectjdcallis just a improvement tojcall(and is still low-level API)I would love
JProxywhile I still proposejdcalldfdx commentedon Nov 25, 2018
First of all, this looks really great! Having said that, I still think you and @zot work on a very similar approaches and your Spark example could be a good demo for JProxy as well. What I'm concerned with is that we may end up in multiple intersecting APIs which would be confusing to occasional users.
I like simplicity and transparency of
jdcall, but it has one important drawback - you callfindmethodand other stuff every time before actualjcall. For Spark it's likely to be a little fraction of work on JVM side, but for other projects it might be a huge overhead.To fix it, one could add a method cache and bind it to an object representing loaded class. At this point it starts looking similar to JProxy approach (though Bill may add more context - I only roughly track implementation changes).
I hope this doesn't discourage you - I really enjoy the described future, just want to keep implementation sound and robust so we could maintain it for many years without breaking things.
dfdx commentedon Nov 25, 2018
By the way, regarding method signatures with different return types:
There are now 2 methods with the same signature except for return type, so your
findmethodwill raise an error. But in general it still works fine - calling a version withJObjectreturn type will invoke correct version from class B, so it's not really funny.Let's make it a bit more interesting and redefine A.java as:
Recompile A.java, but not B.java. This may happen e.g. when combing in runtime different versions of JARs.
JBstill lists 2 method it saw during compilation. Let's call them:So far so good.
Still correct - method from B.
WAT? Now we have 2 methods with exactly the same name and argument list, but with even incompatible return types!
You might think it's just a contrived example, but as I mentioned earlier I've encountered something very similar in practice in Spark.jl - I spent a couple of hours figuring out correct signature for jcall when even
listmethodsdidn't show the one I needed.clouds56 commentedon Nov 26, 2018
Interesting point. I think the limit would also apply to
JProxysince it also uses listmethods to find methods. I wonder how it would handle this.And more than that, what would happen if I call
For the drawback that calling
listmethodsevery time is expensive, I think we could cache methods we found (not caching the whole method list).If you use some class heavily, the
JProxyshould be your choice.There remains some issues.
listmethods()changed?clouds56 commentedon Nov 26, 2018
I realized that the dispatch/findmethod relies on
hasmethod(convert, ...), which means the founded method may differ if moreconvertfunction defined and thusjdcall_cachedgoes stale.Even worse, as introducing more
convertfunctions, some worked code would be broken. image that someone wrote a function like this(at the same time
jdcallcould take the advantage of user definedconvertto more flexibly dispatch method)Maybe we should make "julia to JavaObject" convert more robust.
JavaObjectinArray{T, 1}JavaCall.isConvertiblefor any type of convert involvingJavaObjectdfdx commentedon Nov 26, 2018
We may also encounter issues with this piece:
if we try to use newly defined method at the same world age as we called
eval(see e.g. this discussion). We can overcome it in several ways:@jimport. IIRC, JProxy aims to do this.Base.invokelatestto call a method from future worlds. This adds constant overhead though.clouds56 commentedon Dec 3, 2018
There still an option 4 that use global
Dict{Tuple{JavaObject{T}, Symbol}, Any}to store cached functions. Not sure if the performance would be better/worse thanBase.invokelatest.I think the issue also applies for
JProxy. Consider runningwhen should I make the proxy for
java.lang.ClassLoader? If as early as possible, it would run into trouble that we would proxy lots of classes (only if they are dependencies of some method of java.util.ArrayList). If as lazy as when got a return type that is ClassLoader, it would have same trouble as long as JProxy useevalto gen code.According to the author of
JProxy, thegen.jlis not used now.JProxyis now doing dynamic dispatch (just likejdcall) in current PR.zot commentedon Dec 3, 2018
Yes, it is doing dynamic dispatch and gen.jl is not currently used but JProxy caches the method ids in JClassInfo structures so that saves a lot of work. Once you make a proxy, the class info is saved so it's around the next time you make one.
JProxy only caches classinfo for its class and the ancestors (classes and interfaces). The classes of the method return types and arguments can cause subtypes of java_lang to be defined but not classinfo data. I.e. the method dictionaries are only populated for the JProxy data's class and its ancestors, not for the return types or arguments of the methods it finds. Otherwise it would end up "faulting in" large parts of the JDK when you referenced just about anything (I think one of my earlier versions did cache classinfo for return types and arguments though).
I'm not sure about the eval and invokelatest issue -- is it possible to defer code to run in a later Julia world? To solve a problem like this in JavaScript, I'd do something like:
Maybe yieldto() will work for that? If that works, then you can just defer later code whenever you need to make more definitions. In any case you can just use @Class(a.b.c) to create a static proxy for a.b.c and that will do all the caching.
By the way, you can use a static proxy as a constructor, in addition to providing access to static members, like this:
clouds56 commentedon Dec 4, 2018
I love the idea that use macro to do static dispatch.
I'm working with ClassLoader now and found that caching would mess things up.
Two classes with the same name/identity may be different class (since different ClassLoader, see here)
zot commentedon Dec 4, 2018
Thanks! I just expanded the idea of JavaCall's
@jimport()macro a little bit there.@class()is really just syntactic sugar for a call tostaticproxy(classname).I just added
staticproxy(JClass)andstaticproxy(JProxy{java_lang_Class, false})so you can easily make static proxies on class objects to help with using different class loaders. Now you can say something likestaticproxy(x.getClass().getClassLoader().loadClass(classname)).This will help because
staticproxy(classname)(with only a string argument) uses reflect.jl'sclassforname()function, which uses the context class loader:[-]Some propose[/-][+]Proposal: jdcall and jtypeforclass[/+]