Skip to content

ccall: make distinction of pointer vs name a syntactic distinction #59165

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Compiler/src/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3488,6 +3488,13 @@ end
function abstract_eval_foreigncall(interp::AbstractInterpreter, e::Expr, sstate::StatementState, sv::AbsIntState)
mi = frame_instance(sv)
t = sp_type_rewrap(e.args[2], mi, true)
let fptr = e.args[1]
if !isexpr(fptr, :tuple)
if !hasintersect(widenconst(abstract_eval_value(interp, fptr, sstate, sv)), Ptr)
return RTEffects(Bottom, Any, EFFECTS_THROWS)
end
end
end
for i = 3:length(e.args)
if abstract_eval_value(interp, e.args[i], sstate, sv) === Bottom
return RTEffects(Bottom, Any, EFFECTS_THROWS)
Expand Down
7 changes: 5 additions & 2 deletions Compiler/src/optimize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1429,8 +1429,11 @@ function statement_cost(ex::Expr, line::Int, src::Union{CodeInfo, IRCode}, sptyp
return params.inline_nonleaf_penalty
elseif head === :foreigncall
foreigncall = ex.args[1]
if foreigncall isa QuoteNode && foreigncall.value === :jl_string_ptr
return 1
if isexpr(foreigncall, :tuple, 1)
foreigncall = foreigncall.args[1]
if foreigncall isa QuoteNode && foreigncall.value === :jl_string_ptr
return 1
end
end
return 20
elseif head === :invoke || head === :invoke_modify
Expand Down
6 changes: 4 additions & 2 deletions Compiler/src/ssair/EscapeAnalysis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1037,8 +1037,10 @@ function escape_foreigncall!(astate::AnalysisState, pc::Int, args::Vector{Any})
# NOTE array allocations might have been proven as nothrow (https://github.com/JuliaLang/julia/pull/43565)
nothrow = is_nothrow(astate.ir, pc)
name_info = nothrow ? ⊥ : ThrownEscape(pc)
add_escape_change!(astate, name, name_info)
add_liveness_change!(astate, name, pc)
if !isexpr(name, :tuple)
add_escape_change!(astate, name, name_info)
add_liveness_change!(astate, name, pc)
end
for i = 1:nargs
# we should escape this argument if it is directly called,
# otherwise just impose ThrownEscape if not nothrow
Expand Down
2 changes: 1 addition & 1 deletion Compiler/src/ssair/inlining.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1755,7 +1755,7 @@ function late_inline_special_case!(ir::IRCode, idx::Int, stmt::Expr, flag::UInt3
length(stmt.args) == 2 ? Any : stmt.args[end])
return SomeCase(typevar_call)
elseif f === UnionAll && length(argtypes) == 3 && ⊑(optimizer_lattice(state.interp), argtypes[2], TypeVar)
unionall_call = Expr(:foreigncall, QuoteNode(:jl_type_unionall), Any, svec(Any, Any),
unionall_call = Expr(:foreigncall, Expr(:tuple, QuoteNode(:jl_type_unionall)), Any, svec(Any, Any),
0, QuoteNode(:ccall), stmt.args[2], stmt.args[3])
return SomeCase(unionall_call)
elseif is_return_type(f)
Expand Down
8 changes: 4 additions & 4 deletions Compiler/src/ssair/verify.jl
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ function check_op(ir::IRCode, domtree::DomTree, @nospecialize(op), use_bb::Int,
end
elseif isa(op, Expr)
# Only Expr(:boundscheck) is allowed in value position
if isforeigncall && arg_idx == 1 && op.head === :call
# Allow a tuple in symbol position for foreigncall - this isn't actually
# a real call - it's interpreted in global scope by codegen. However,
# we do need to keep this a real use, because it could also be a pointer.
if isforeigncall && arg_idx == 1 && op.head === :tuple
# Allow a tuple literal in symbol position for foreigncall - this
# is syntax for a literal value or globalref - it is interpreted in
# global scope by codegen.
elseif !is_value_pos_expr_head(op.head)
if !allow_frontend_forms || op.head !== :opaque_closure_method
@verify_error "Expr not allowed in value position"
Expand Down
14 changes: 11 additions & 3 deletions Compiler/src/verifytrim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -279,9 +279,17 @@ function verify_codeinstance!(interp::NativeInterpreter, codeinst::CodeInstance,
error = "unresolved cfunction"
elseif isexpr(stmt, :foreigncall)
foreigncall = stmt.args[1]
if foreigncall isa QuoteNode
if foreigncall.value in runtime_functions
error = "disallowed ccall into a runtime function"
if isexpr(foreigncall, :tuple, 1)
foreigncall = foreigncall.args[1]
if foreigncall isa String
foreigncall = QuoteNode(Symbol(foreigncall))
end
if foreigncall isa QuoteNode
if foreigncall.value in runtime_functions
error = "disallowed ccall into a runtime function"
end
else
error = "disallowed ccall with non-constant name and no library"
end
end
elseif isexpr(stmt, :new_opaque_closure)
Expand Down
2 changes: 1 addition & 1 deletion Compiler/test/irpasses.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1071,7 +1071,7 @@ let # Test for https://github.com/JuliaLang/julia/issues/43402
end

refs = map(Core.SSAValue, findall(@nospecialize(x)->Meta.isexpr(x, :new), src.code))
some_ccall = findfirst(@nospecialize(x) -> Meta.isexpr(x, :foreigncall) && x.args[1] == :(:some_ccall), src.code)
some_ccall = findfirst(@nospecialize(x) -> Meta.isexpr(x, :foreigncall) && x.args[1] == Expr(:tuple, :(:some_ccall)), src.code)
@assert some_ccall !== nothing
stmt = src.code[some_ccall]
nccallargs = length(stmt.args[3]::Core.SimpleVector)
Expand Down
34 changes: 9 additions & 25 deletions base/c.jl
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,15 @@ function ccall_macro_parse(exprs)
# get the function symbols
func = let f = call.args[1]
if isexpr(f, :.)
:(($(f.args[2]), $(f.args[1])))
Expr(:tuple, f.args[2], f.args[1])
elseif isexpr(f, :$)
f
func = f.args[1]
if isa(func, String) || (isa(func, QuoteNode) && !isa(func.value, Ptr)) || isa(func, Tuple) || isexpr(func, :tuple)
throw(ArgumentError("interpolated value should be a variable or expression, not a literal name or tuple"))
end
func
elseif f isa Symbol
QuoteNode(f)
Expr(:tuple, QuoteNode(f))
else
throw(ArgumentError("@ccall function name must be a symbol, a `.` node (e.g. `libc.printf`) or an interpolated function pointer (with `\$`)"))
end
Expand Down Expand Up @@ -363,33 +367,13 @@ end


function ccall_macro_lower(convention, func, rettype, types, args, gc_safe, nreq)
statements = []

# if interpolation was used, ensure the value is a function pointer at runtime.
if isexpr(func, :$)
push!(statements, Expr(:(=), :func, esc(func.args[1])))
name = QuoteNode(func.args[1])
func = :func
check = quote
if !isa(func, Ptr{Cvoid})
name = $name
throw(ArgumentError(LazyString("interpolated function `", name, "` was not a Ptr{Cvoid}, but ", typeof(func))))
end
end
push!(statements, check)
else
func = esc(func)
end
cconv = nothing
if convention isa Tuple
cconv = Expr(:cconv, (convention..., gc_safe), nreq)
else
cconv = Expr(:cconv, (convention, UInt16(0), gc_safe), nreq)
end

return Expr(:block, statements...,
Expr(:call, :ccall, func, cconv, esc(rettype),
Expr(:tuple, map(esc, types)...), map(esc, args)...))
return Expr(:call, :ccall, esc(func), cconv, esc(rettype),
Expr(:tuple, map(esc, types)...), map(esc, args)...)
end

"""
Expand Down
4 changes: 2 additions & 2 deletions base/gmp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ using ..GMP: BigInt, Limb, BITS_PER_LIMB, libgmp
const mpz_t = Ref{BigInt}
const bitcnt_t = Culong

gmpz(op::Symbol) = (Symbol(:__gmpz_, op), libgmp)
gmpz(op::Symbol) = Expr(:tuple, QuoteNode(Symbol(:__gmpz_, op)), GlobalRef(MPZ, :libgmp))
Copy link
Member

Choose a reason for hiding this comment

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

I wouldn't be surprised if some other packages do this. We might need to allow it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we might, but until PkgEval results finish, we won't know if we need to add yet more cases or are fine with what we already have covering all uses. It is fairly easy to add (it is the same as the syntactic string case), but we won't know if it worth documenting yet another optional form of the same thing until we finish the test run.


init!(x::BigInt) = (ccall((:__gmpz_init, libgmp), Cvoid, (mpz_t,), x); x)
init2!(x::BigInt, a) = (ccall((:__gmpz_init2, libgmp), Cvoid, (mpz_t, bitcnt_t), x, a); x)
Expand Down Expand Up @@ -917,7 +917,7 @@ module MPQ
import .Base: unsafe_rational, __throw_rational_argerror_zero
import ..GMP: BigInt, MPZ, Limb, libgmp

gmpq(op::Symbol) = (Symbol(:__gmpq_, op), libgmp)
gmpq(op::Symbol) = Expr(:tuple, QuoteNode(Symbol(:__gmpq_, op)), GlobalRef(MPZ, :libgmp))

mutable struct _MPQ
num_alloc::Cint
Expand Down
2 changes: 1 addition & 1 deletion doc/src/devdocs/llvm.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ need to make sure that the array does stay alive while we're doing the
[`ccall`](@ref). To understand how this is done, lets look at a hypothetical
approximate possible lowering of the above code:
```julia
return $(Expr(:foreigncall, :(:foo), Cvoid, svec(Ptr{Float64}), 0, :(:ccall), Expr(:foreigncall, :(:jl_array_ptr), Ptr{Float64}, svec(Any), 0, :(:ccall), :(A)), :(A)))
return $(Expr(:foreigncall, Expr(:tuple, :(:foo)), Cvoid, svec(Ptr{Float64}), 0, :(:ccall), Expr(:foreigncall, Expr(:tuple, :(:jl_array_ptr)), Ptr{Float64}, svec(Any), 0, :(:ccall), :(A)), :(A)))
```
The last `:(A)`, is an extra argument list inserted during lowering that informs
the code generator which Julia level values need to be kept alive for the
Expand Down
68 changes: 51 additions & 17 deletions doc/src/manual/calling-c-and-fortran-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ The syntax for [`@ccall`](@ref) to generate a call to the library function is:
@ccall $function_pointer(argvalue1::argtype1, ...)::returntype
```

where `library` is a string constant or literal (but see [Non-constant Function
Specifications](@ref) below). The library may be omitted, in which case the
function name is resolved in the current process. This form can be used to call
C library functions, functions in the Julia runtime, or functions in an
application linked to Julia. The full path to the library may also be specified.
where `library` is a string constant or global variable name (see [Non-constant
Function -Specifications](@ref) below). The library can be just a name, or it
can specify a full path to the library. The library may be omitted, in which
case the function name is resolved in the current executable, the current libc,
or libjulia(-internal). This form can be used to call C library functions,
functions in the Julia runtime, or functions in an application linked to Julia.
Omitting the library *cannot* be used to call a function in any library (like
specifying `RTLD_DEFAULT` to `dlsym`) as such behavior is slow, complicated,
and not implemented on all platforms.
Alternatively, `@ccall` may also be used to call a function pointer
`$function_pointer`, such as one returned by `Libdl.dlsym`. The `argtype`s
corresponds to the C-function signature and the `argvalue`s are the actual
Expand Down Expand Up @@ -848,13 +852,17 @@ it must be handled in other ways.

## Non-constant Function Specifications

In some cases, the exact name or path of the needed library is not known in advance and must
be computed at run time. To handle such cases, the library component
specification can be a function call, e.g. `find_blas().dgemm`. The call expression will
be executed when the `ccall` itself is executed. However, it is assumed that the library
location does not change once it is determined, so the result of the call can be cached and
reused. Therefore, the number of times the expression executes is unspecified, and returning
different values for multiple calls results in unspecified behavior.
In some cases, the exact name or path of the needed library is not known in
advance and must be computed at run time. To handle such cases, the library
component specification can be a value such as `Libdl.LazyLibrary`. For
example, in `@ccall blas.dgemm()`, there can be a global defined as `const blas
= LazyLibrary("libblas")`. The runtime will call `dlsym(:dgemm, dlopen(blas))`
when the `@ccall` itself is executed. The `Libdl.dlopen` function can be
overloaded for custom types to provide alternate behaviors. However, it is
assumed that the library location does not change once it is determined, so the
result of the call can be cached and reused. Therefore, the number of times the
expression executes is unspecified, and returning different values for multiple
calls results in unspecified behavior.

If even more flexibility is needed, it is possible
to use computed values as function names by staging through [`eval`](@ref) as follows:
Expand Down Expand Up @@ -990,11 +998,37 @@ The arguments to [`ccall`](@ref) are:


!!! note
The `(:function, "library")` pair, return type, and input types must be literal constants
(i.e., they can't be variables, but see [Non-constant Function Specifications](@ref)).

The remaining parameters are evaluated at compile-time, when the containing method is defined.

The `(:function, "library")` pair and the input type list must be syntactic tuples
(i.e., they can't be variables or values with a type of Tuple.

The rettype and argument type values are evaluated at when the containing method is
defined, not runtime.

!!! note "Function Name vs Pointer Syntax"
The syntax of the first argument to `ccall` determines whether you're calling by **name** or by **pointer**:
* **Name-based calls** (tuple literal syntax):
- Both the function and library names can be a quoted Symbol, a String, a
variable name (a GlobalRef), or a dotted expression ending with a variable
name.
- Single name: `(:function_name,)` or `"function_name"` - uses default library lookup.
- Name with library: `(:function_name, "library")` - specifies both function and library.
- Symbol and string literals are automatically normalized to tuple form.
* **Pointer-based calls** (non-tuple syntax):
- Anything that is not a literal tuple expression specified above is assumed to be an
expression that evaluates to a function pointers at runtime.
- Function pointer variables: `fptr` where `fptr` is a runtime pointer value.
- Function pointer computations: `dlsym(:something)` where the result is computed at
runtime every time (usually along with some caching logic).
* **Library name expressions**:
- When given as a variable, the library name can resolve to a `Symbol`, a `String`, or
any other value. The runtime will call `Libdl.dlopen(name)` on the value an
unspecified number of times, caching the result. The result is not invalidated if the
value of the binding changes or if it becomes undefined, as long as there exists any
value for that binding in any past or future worlds, that value may be used.
- Dot expressions, such as `A.B().c`, will be executed at method definition
time up to the final `c`. The first part must resolve to a Module, and the
second part to a quoted symbol. The value of that global will be resolved at
runtime when the `ccall` is first executed.

A table of translations between the macro and function interfaces is given below.

Expand Down
Loading
Loading