Skip to content

Commit

Permalink
Add prototype jcall macro (#165)
Browse files Browse the repository at this point in the history
* Add prototype jcall macro

* Improve @jcall macro

* Remove nreq from macro, restore 1.0 compat
  • Loading branch information
mkitti authored Aug 15, 2023
1 parent d7f3136 commit 2b8c3f1
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/JavaCall.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export JavaObject, JavaMetaClass, JNIVector,
jint, jlong, jbyte, jboolean, jchar, jshort, jfloat, jdouble, jvoid,
JObject, JClass, JMethod, JConstructor, JField, JString,
JavaRef, JavaLocalRef, JavaGlobalRef, JavaNullRef,
@jimport, jcall, jfield, jlocalframe, isnull,
@jimport, @jcall, jcall, jfield, jlocalframe, isnull,
getname, getclass, listmethods, getreturntype, getparametertypes, classforname,
listfields, gettype,
narrow
Expand Down Expand Up @@ -40,6 +40,7 @@ include("core.jl")
include("convert.jl")
include("reflect.jl")
include("jniarray.jl")
include("jcall_macro.jl")

Base.@deprecate_binding jnifunc JavaCall.JNI.jniref[]

Expand Down
2 changes: 2 additions & 0 deletions src/core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,8 @@ function jfield(ref, field::AbstractString)
_jfield(_jcallable(ref), jfieldID, fieldType)
end

jfield(ref, field::Symbol) = jfield(ref, String(field))

function get_field_id(typ::Type{JavaObject{T}}, field::AbstractString, fieldType::Type) where T
@checknull JNI.GetStaticFieldID(Ptr(metaclass(T)), String(field), signature(fieldType))
end
Expand Down
90 changes: 90 additions & 0 deletions src/jcall_macro.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
macro jcall(expr)
return jcall_macro_lower(jcall_macro_parse(expr)...)
end

function jcall_macro_lower(func, rettype, types, args)
@debug "args: " func rettype types args
jtypes = Expr(:tuple, esc.(types)...)
jargs = Expr(:tuple, esc.(args)...)
jret = esc(rettype)
if func isa Expr
@debug "func" func.head func.args
obj = resolve_dots(func.args[2])
f = string(func.args[1].value)
return :(jcall($(esc(obj)), $f, $jret, $jtypes, ($jargs)...))
elseif func isa QuoteNode
return :($(esc(func.value))($jtypes, ($jargs)...))
end
end

function resolve_dots(obj)
if obj isa Expr && obj.head == :.
return :(jfield($(resolve_dots(obj.args[1])), string($(obj.args[2]))))
else
return obj
end
end

# @jcall implementation, based on Base.@ccall
"""
jcall_macro_parse(expression)
`jcall_macro_parse` is an implementation detail of `@jcall
it takes an expression like `:(System.out.println("Hello"::JString)::Nothing)`
returns: a tuple of `(function_name, return_type, arg_types, args)`
The above input outputs this:
(:(System.out.println), Nothing, [:JString], ["Hello])
"""
function jcall_macro_parse(expr::Expr)
# setup and check for errors
if !Meta.isexpr(expr, :(::))
throw(ArgumentError("@jcall needs a function signature with a return type"))
end
rettype = expr.args[2]

call = expr.args[1]
if !Meta.isexpr(call, :call)
throw(ArgumentError("@jcall has to take a function call"))
end

# get the function symbols
func = let f = call.args[1]
if Meta.isexpr(f, :.)
:(($(f.args[2]), $(f.args[1])))
elseif Meta.isexpr(f, :$)
f
elseif f isa Symbol
QuoteNode(f)
else
throw(ArgumentError("@jcall function name must be a symbol or a `.` node (e.g. `System.out.println`)"))
end
end

# detect varargs
varargs = nothing
argstart = 2
callargs = call.args
if length(callargs) >= 2 && Meta.isexpr(callargs[2], :parameters)
argstart = 3
varargs = callargs[2].args
end

# collect args and types
args = []
types = []

function pusharg!(arg)
if !Meta.isexpr(arg, :(::))
throw(ArgumentError("args in @jcall need type annotations. '$arg' doesn't have one."))
end
push!(args, arg.args[1])
push!(types, arg.args[2])
end

for i in argstart:length(callargs)
pusharg!(callargs[i])
end

return func, rettype, types, args
end

55 changes: 55 additions & 0 deletions test/jcall_macro.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Test
using JavaCall

@testset "jcall macro" begin
JavaCall.isloaded() || JavaCall.init(["-Djava.class.path=$(@__DIR__)"])
System = @jimport java.lang.System
version_from_macro = @jcall System.getProperty("java.version"::JString)::JString
version_from_func = jcall(System, "getProperty", JString, (JString,), "java.version")
@test version_from_macro == version_from_func
@test "bar" == @jcall System.getProperty("foo"::JString, "bar"::JString)::JString
@test 0x00 == @jcall System.out.checkError()::jboolean
rettype = jboolean
@test 0x00 == @jcall System.out.checkError()::rettype
jstr = JString
@test version_from_func == @jcall System.getProperty("java.version"::jstr)::jstr

T = @jimport Test
@test 10 == @jcall T.testShort(10::jshort)::jshort
@test 10 == @jcall T.testInt(10::jint)::jint
@test 10 == @jcall T.testLong(10::jlong)::jlong
@test typemax(jint) == @jcall T.testInt(typemax(jint)::jint)::jint
@test typemax(jlong) == @jcall T.testLong(typemax(jlong)::jlong)::jlong
@test "Hello Java" == @jcall T.testString("Hello Java"::JString)::JString
@test Float64(10.02) == @jcall T.testDouble(10.02::jdouble)::jdouble
@test Float32(10.02) == @jcall T.testFloat(10.02::jfloat)::jfloat
@test floatmax(jdouble) == @jcall T.testDouble(floatmax(jdouble)::jdouble)::jdouble
@test floatmax(jfloat) == @jcall T.testFloat(floatmax(jfloat)::jfloat)::jfloat
c=JString(C_NULL)
@test isnull(c)
@test "" == @jcall T.testString(c::JString)::JString
a = rand(10^7)
@test [@jcall(T.testDoubleArray(a::Array{jdouble,1})::jdouble)
for i in 1:10][1] sum(a)
a = nothing

jlm = @jimport "java.lang.Math"
@test 1.0 @jcall jlm.sin((pi/2)::jdouble)::jdouble
@test 1.0 @jcall jlm.min(1::jdouble, 2::jdouble)::jdouble
@test 1 == @jcall jlm.abs((-1)::jint)::jint

@testset "jcall macro instance_methods_1" begin
jnu = @jimport java.net.URL
gurl = @jcall jnu("https://en.wikipedia.org"::JString)::jnu
@test "en.wikipedia.org"== @jcall gurl.getHost()::JString
jni = @jimport java.net.URI
guri = @jcall gurl.toURI()::jni
@test typeof(guri)==jni

h=@jcall guri.hashCode()::jint
@test typeof(h)==jint
end

jlist = @jimport java.util.ArrayList
@test 0x01 == @jcall jlist().add(JObject(C_NULL)::JObject)::jboolean
end
2 changes: 2 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,8 @@ end
end
end

include("jcall_macro.jl")

end

# Test downstream dependencies
Expand Down

2 comments on commit 2b8c3f1

@mkitti
Copy link
Member Author

@mkitti mkitti commented on 2b8c3f1 Aug 15, 2023

Choose a reason for hiding this comment

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

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

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

Registration pull request created: JuliaRegistries/General/89671

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.8.0 -m "<description of version>" 2b8c3f162662233802826a05f358961876a247b9
git push origin v0.8.0

Please sign in to comment.