Skip to content

Commit be920b7

Browse files
ninjaaronravibitsgoa
authored andcommitted
A more julian syntax for ccall: the @ ccall macro (JuliaLang#32748)
Here we implement a syntax for ccall with Julia-like type annotations on the arguments. Compared to ccall: * The new syntax is more familiar to Julia programmers * The new syntax gives a notation for calling C varargs functions * Support for specifying calling convention is not yet implemented as that will require another syntax discussion * The semantics for interpolating "function like things" is much simplified (only function pointers are allowed)
1 parent 0fb41bf commit be920b7

File tree

4 files changed

+281
-1
lines changed

4 files changed

+281
-1
lines changed

NEWS.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Build system changes
4646

4747
New library functions
4848
---------------------
49-
49+
* The `@ccall` macro has been added added to Base. It is a near drop-in replacement for `ccall` with more Julia-like syntax. It also wraps the new `foreigncall` API for varargs of different types, though it lacks the the capability to specify an LLVM calling convention. ([#32748])
5050
* New functions `mergewith` and `mergewith!` supersede `merge` and `merge!` with `combine`
5151
argument. They don't have the restriction for `combine` to be a `Function` and also
5252
provide one-argument method that returns a closure. The old methods of `merge` and

base/c.jl

+173
Original file line numberDiff line numberDiff line change
@@ -503,3 +503,176 @@ end
503503
macro ccallable(rt, def)
504504
expand_ccallable(rt, def)
505505
end
506+
507+
# @ccall implementation
508+
"""
509+
ccall_macro_parse(expression)
510+
511+
`ccall_macro_parse` is an implementation detail of `@ccall
512+
513+
it takes an expression like `:(printf("%d"::Cstring, value::Cuint)::Cvoid)`
514+
returns: a tuple of `(function_name, return_type, arg_types, args)`
515+
516+
The above input outputs this:
517+
518+
(:printf, :Cvoid, [:Cstring, :Cuint], ["%d", :value])
519+
"""
520+
function ccall_macro_parse(expr::Expr)
521+
# setup and check for errors
522+
if !Meta.isexpr(expr, :(::))
523+
throw(ArgumentError("@ccall needs a function signature with a return type"))
524+
end
525+
rettype = expr.args[2]
526+
527+
call = expr.args[1]
528+
if !Meta.isexpr(call, :call)
529+
throw(ArgumentError("@ccall has to take a function call"))
530+
end
531+
532+
# get the function symbols
533+
func = let f = call.args[1]
534+
if Meta.isexpr(f, :.)
535+
:(($(f.args[2]), $(f.args[1])))
536+
elseif Meta.isexpr(f, :$)
537+
f
538+
elseif f isa Symbol
539+
QuoteNode(f)
540+
else
541+
throw(ArgumentError("@ccall function name must be a symbol, a `.` node (e.g. `libc.printf`) or an interpolated function pointer (with `\$`)"))
542+
end
543+
end
544+
545+
# detect varargs
546+
varargs = nothing
547+
argstart = 2
548+
callargs = call.args
549+
if length(callargs) >= 2 && Meta.isexpr(callargs[2], :parameters)
550+
argstart = 3
551+
varargs = callargs[2].args
552+
end
553+
554+
# collect args and types
555+
args = []
556+
types = []
557+
558+
function pusharg!(arg)
559+
if !Meta.isexpr(arg, :(::))
560+
throw(ArgumentError("args in @ccall need type annotations. '$arg' doesn't have one."))
561+
end
562+
push!(args, arg.args[1])
563+
push!(types, arg.args[2])
564+
end
565+
566+
for i in argstart:length(callargs)
567+
pusharg!(callargs[i])
568+
end
569+
# add any varargs if necessary
570+
nreq = 0
571+
if !isnothing(varargs)
572+
if length(args) == 0
573+
throw(ArgumentError("C ABI prohibits vararg without one required argument"))
574+
end
575+
nreq = length(args)
576+
for a in varargs
577+
pusharg!(a)
578+
end
579+
end
580+
581+
return func, rettype, types, args, nreq
582+
end
583+
584+
585+
function ccall_macro_lower(convention, func, rettype, types, args, nreq)
586+
lowering = []
587+
realargs = []
588+
gcroots = []
589+
590+
# if interpolation was used, ensure variable is a function pointer at runtime.
591+
if Meta.isexpr(func, :$)
592+
push!(lowering, Expr(:(=), :func, esc(func.args[1])))
593+
name = QuoteNode(func.args[1])
594+
func = :func
595+
check = quote
596+
if !isa(func, Ptr{Cvoid})
597+
name = $name
598+
throw(ArgumentError("interpolated function `$name` was not a Ptr{Cvoid}, but $(typeof(func))"))
599+
end
600+
end
601+
push!(lowering, check)
602+
else
603+
func = esc(func)
604+
end
605+
606+
for (i, (arg, type)) in enumerate(zip(args, types))
607+
sym = Symbol(string("arg", i, "root"))
608+
sym2 = Symbol(string("arg", i, ))
609+
earg, etype = esc(arg), esc(type)
610+
push!(lowering, :($sym = Base.cconvert($etype, $earg)))
611+
push!(lowering, :($sym2 = Base.unsafe_convert($etype, $sym)))
612+
push!(realargs, sym2)
613+
push!(gcroots, sym)
614+
end
615+
etypes = Expr(:call, Expr(:core, :svec), types...)
616+
exp = Expr(:foreigncall,
617+
func,
618+
esc(rettype),
619+
esc(etypes),
620+
nreq,
621+
QuoteNode(convention),
622+
realargs..., gcroots...)
623+
push!(lowering, exp)
624+
625+
return Expr(:block, lowering...)
626+
end
627+
628+
"""
629+
@ccall library.function_name(argvalue1::argtype1, ...)::returntype
630+
@ccall function_name(argvalue1::argtype1, ...)::returntype
631+
@ccall \$function_pointer(argvalue1::argtype1, ...)::returntype
632+
633+
Call a function in a C-exported shared library, specified by
634+
`library.function_name`, where `library` is a string constant or
635+
literal. The library may be omitted, in which case the `function_name`
636+
is resolved in the current process. Alternatively, `@ccall` may
637+
also be used to call a function pointer `\$function_pointer`, such as
638+
one returned by `dlsym`.
639+
640+
Each `argvalue` to `@ccall` is converted to the corresponding
641+
`argtype`, by automatic insertion of calls to `unsafe_convert(argtype,
642+
cconvert(argtype, argvalue))`. (See also the documentation for
643+
[`unsafe_convert`](@ref Base.unsafe_convert) and [`cconvert`](@ref
644+
Base.cconvert) for further details.) In most cases, this simply
645+
results in a call to `convert(argtype, argvalue)`.
646+
647+
648+
# Examples
649+
650+
@ccall strlen(s::Cstring)::Csize_t
651+
652+
This calls the C standard library function:
653+
654+
size_t strlen(char *)
655+
656+
with a Julia variable named `s`. See also `ccall`.
657+
658+
Varargs are supported with the following convention:
659+
660+
@ccall sprintf("%s = %d"::Cstring ; "foo"::Cstring, foo::Cint)::Cint
661+
662+
The semicolon is used to separate required arguments (of which there
663+
must be at least one) from variadic arguments.
664+
665+
Example using an external library:
666+
667+
# C signature of g_uri_escape_string:
668+
# char *g_uri_escape_string(const char *unescaped, const char *reserved_chars_allowed, gboolean allow_utf8);
669+
670+
const glib = "libglib-2.0"
671+
@ccall glib.g_uri_escape_string(my_uri::Cstring, ":/"::Cstring, true::Cint)::Cstring
672+
673+
The string literal could also be used directly before the function
674+
name, if desired `"libglib-2.0".g_uri_escape_string(...`
675+
"""
676+
macro ccall(expr)
677+
return ccall_macro_lower(:ccall, ccall_macro_parse(expr)...)
678+
end

base/exports.jl

+1
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,7 @@ export
911911

912912
# C interface
913913
@cfunction,
914+
@ccall,
914915
cglobal,
915916
disable_sigint,
916917
pointer,

test/ccall.jl

+106
Original file line numberDiff line numberDiff line change
@@ -1574,3 +1574,109 @@ let
15741574
ccall(:memcpy, Ptr{Cvoid}, (Ref{Int}, Ref{Int}, Csize_t), dest, src, 3*sizeof(Int))
15751575
@test dest[] == (7,8,9)
15761576
end
1577+
1578+
1579+
# @ccall macro
1580+
using Base: ccall_macro_parse, ccall_macro_lower
1581+
@testset "test basic ccall_macro_parse functionality" begin
1582+
callexpr = :(
1583+
libc.printf("%s = %d\n"::Cstring ; name::Cstring, value::Cint)::Cvoid
1584+
)
1585+
@test ccall_macro_parse(callexpr) == (
1586+
:((:printf, libc)), # function
1587+
:Cvoid, # returntype
1588+
Any[:Cstring, :Cstring, :Cint], # argument types
1589+
Any["%s = %d\n", :name, :value], # argument symbols
1590+
1 # number of required arguments (for varargs)
1591+
)
1592+
end
1593+
1594+
@testset "ensure the base-case of @ccall works, including library name and pointer interpolation" begin
1595+
call = ccall_macro_lower(:ccall, ccall_macro_parse( :( libstring.func(
1596+
str::Cstring,
1597+
num1::Cint,
1598+
num2::Cint
1599+
)::Cstring))...)
1600+
@test call == Base.remove_linenums!(
1601+
quote
1602+
arg1root = Base.cconvert($(Expr(:escape, :Cstring)), $(Expr(:escape, :str)))
1603+
arg1 = Base.unsafe_convert($(Expr(:escape, :Cstring)), arg1root)
1604+
arg2root = Base.cconvert($(Expr(:escape, :Cint)), $(Expr(:escape, :num1)))
1605+
arg2 = Base.unsafe_convert($(Expr(:escape, :Cint)), arg2root)
1606+
arg3root = Base.cconvert($(Expr(:escape, :Cint)), $(Expr(:escape, :num2)))
1607+
arg3 = Base.unsafe_convert($(Expr(:escape, :Cint)), arg3root)
1608+
$(Expr(:foreigncall,
1609+
:($(Expr(:escape, :((:func, libstring))))),
1610+
:($(Expr(:escape, :Cstring))),
1611+
:($(Expr(:escape, :(($(Expr(:core, :svec)))(Cstring, Cint, Cint))))),
1612+
0,
1613+
:(:ccall),
1614+
:arg1, :arg2, :arg3, :arg1root, :arg2root, :arg3root))
1615+
end)
1616+
1617+
# pointer interpolation
1618+
call = ccall_macro_lower(:ccall, ccall_macro_parse(:( $(Expr(:$, :fptr))("bar"::Cstring)::Cvoid ))...)
1619+
@test Base.remove_linenums!(call) == Base.remove_linenums!(
1620+
quote
1621+
func = $(Expr(:escape, :fptr))
1622+
begin
1623+
if !(func isa Ptr{Cvoid})
1624+
name = :fptr
1625+
throw(ArgumentError("interpolated function `$(name)` was not a Ptr{Cvoid}, but $(typeof(func))"))
1626+
end
1627+
end
1628+
arg1root = Base.cconvert($(Expr(:escape, :Cstring)), $(Expr(:escape, "bar")))
1629+
arg1 = Base.unsafe_convert($(Expr(:escape, :Cstring)), arg1root)
1630+
$(Expr(:foreigncall, :func, :($(Expr(:escape, :Cvoid))), :($(Expr(:escape, :(($(Expr(:core, :svec)))(Cstring))))), 0, :(:ccall), :arg1, :arg1root))
1631+
end)
1632+
1633+
end
1634+
1635+
@testset "check error paths" begin
1636+
# missing return type
1637+
@test_throws ArgumentError("@ccall needs a function signature with a return type") ccall_macro_parse(:( foo(4.0::Cdouble )))
1638+
# not a function call
1639+
@test_throws ArgumentError("@ccall has to take a function call") ccall_macro_parse(:( foo::Type ))
1640+
# missing type annotations on arguments
1641+
@test_throws ArgumentError("args in @ccall need type annotations. 'x' doesn't have one.") ccall_macro_parse(:( foo(x)::Cint ))
1642+
# missing type annotations on varargs arguments
1643+
@test_throws ArgumentError("args in @ccall need type annotations. 'y' doesn't have one.") ccall_macro_parse(:( foo(x::Cint ; y)::Cint ))
1644+
# no reqired args on varargs call
1645+
@test_throws ArgumentError("C ABI prohibits vararg without one required argument") ccall_macro_parse(:( foo(; x::Cint)::Cint ))
1646+
# not a function pointer
1647+
@test_throws ArgumentError("interpolated function `PROGRAM_FILE` was not a Ptr{Cvoid}, but String") @ccall $PROGRAM_FILE("foo"::Cstring)::Cvoid
1648+
end
1649+
1650+
# call some c functions
1651+
@testset "run @ccall with C standard library functions" begin
1652+
@test @ccall(sqrt(4.0::Cdouble)::Cdouble) == 2.0
1653+
1654+
str = "hello"
1655+
buf = Ptr{UInt8}(Libc.malloc((length(str) + 1) * sizeof(Cchar)))
1656+
@ccall strcpy(buf::Cstring, str::Cstring)::Cstring
1657+
@test unsafe_string(buf) == str
1658+
Libc.free(buf)
1659+
1660+
# test pointer interpolation
1661+
str_identity = @cfunction(identity, Cstring, (Cstring,))
1662+
foo = @ccall $str_identity("foo"::Cstring)::Cstring
1663+
@test unsafe_string(foo) == "foo"
1664+
# test interpolation of an expresison that returns a pointer.
1665+
foo = @ccall $(@cfunction(identity, Cstring, (Cstring,)))("foo"::Cstring)::Cstring
1666+
@test unsafe_string(foo) == "foo"
1667+
1668+
# test of a vararg foreigncall using @ccall
1669+
strp = Ref{Ptr{Cchar}}(0)
1670+
fmt = "hi+%hhd-%hhd-%hhd-%hhd-%hhd-%hhd-%hhd-%hhd-%hhd-%hhd-%hhd-%hhd-%hhd-%hhd-%hhd-%.1f-%.1f-%.1f-%.1f-%.1f-%.1f-%.1f-%.1f-%.1f\n"
1671+
1672+
len = @ccall asprintf(
1673+
strp::Ptr{Ptr{Cchar}},
1674+
fmt::Cstring,
1675+
; # begin varargs
1676+
0x1::UInt8, 0x2::UInt8, 0x3::UInt8, 0x4::UInt8, 0x5::UInt8, 0x6::UInt8, 0x7::UInt8, 0x8::UInt8, 0x9::UInt8, 0xa::UInt8, 0xb::UInt8, 0xc::UInt8, 0xd::UInt8, 0xe::UInt8, 0xf::UInt8,
1677+
1.1::Cfloat, 2.2::Cfloat, 3.3::Cfloat, 4.4::Cfloat, 5.5::Cfloat, 6.6::Cfloat, 7.7::Cfloat, 8.8::Cfloat, 9.9::Cfloat,
1678+
)::Cint
1679+
str = unsafe_string(strp[], len)
1680+
@ccall free(strp[]::Cstring)::Cvoid
1681+
@test str == "hi+1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-1.1-2.2-3.3-4.4-5.5-6.6-7.7-8.8-9.9\n"
1682+
end

0 commit comments

Comments
 (0)