diff --git a/Compiler/src/abstractinterpretation.jl b/Compiler/src/abstractinterpretation.jl index 100cdd97e511a..13e2ffb4ac117 100644 --- a/Compiler/src/abstractinterpretation.jl +++ b/Compiler/src/abstractinterpretation.jl @@ -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) diff --git a/Compiler/src/optimize.jl b/Compiler/src/optimize.jl index 27fb1e2168639..aa898c71aef04 100644 --- a/Compiler/src/optimize.jl +++ b/Compiler/src/optimize.jl @@ -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 diff --git a/Compiler/src/ssair/EscapeAnalysis.jl b/Compiler/src/ssair/EscapeAnalysis.jl index 4ce972937700c..d78ec529cbd9f 100644 --- a/Compiler/src/ssair/EscapeAnalysis.jl +++ b/Compiler/src/ssair/EscapeAnalysis.jl @@ -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 diff --git a/Compiler/src/ssair/inlining.jl b/Compiler/src/ssair/inlining.jl index 251767d577157..e4a7275657b3f 100644 --- a/Compiler/src/ssair/inlining.jl +++ b/Compiler/src/ssair/inlining.jl @@ -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) diff --git a/Compiler/src/ssair/verify.jl b/Compiler/src/ssair/verify.jl index 3aa1e00e3f2d3..69a0e36eb1f17 100644 --- a/Compiler/src/ssair/verify.jl +++ b/Compiler/src/ssair/verify.jl @@ -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" diff --git a/Compiler/src/verifytrim.jl b/Compiler/src/verifytrim.jl index eb775bfa290ce..2f3d615adc3d1 100644 --- a/Compiler/src/verifytrim.jl +++ b/Compiler/src/verifytrim.jl @@ -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) diff --git a/Compiler/test/irpasses.jl b/Compiler/test/irpasses.jl index 758efaab9ab6b..d100bed12e716 100644 --- a/Compiler/test/irpasses.jl +++ b/Compiler/test/irpasses.jl @@ -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) diff --git a/base/c.jl b/base/c.jl index 69ea3adf24404..ba4b237eee406 100644 --- a/base/c.jl +++ b/base/c.jl @@ -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 @@ -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 """ diff --git a/base/gmp.jl b/base/gmp.jl index e4d9294766aaa..0a1cd90f822fc 100644 --- a/base/gmp.jl +++ b/base/gmp.jl @@ -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)) 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) @@ -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 diff --git a/doc/src/devdocs/llvm.md b/doc/src/devdocs/llvm.md index 480bca6fa6ecf..3db57a59deb06 100644 --- a/doc/src/devdocs/llvm.md +++ b/doc/src/devdocs/llvm.md @@ -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 diff --git a/doc/src/manual/calling-c-and-fortran-code.md b/doc/src/manual/calling-c-and-fortran-code.md index bbc3ccdeb6fc0..535f584f5cee2 100644 --- a/doc/src/manual/calling-c-and-fortran-code.md +++ b/doc/src/manual/calling-c-and-fortran-code.md @@ -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 @@ -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: @@ -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. diff --git a/src/ccall.cpp b/src/ccall.cpp index d9d25c7a939f0..029809eff1970 100644 --- a/src/ccall.cpp +++ b/src/ccall.cpp @@ -54,13 +54,43 @@ GlobalVariable *jl_emit_RTLD_DEFAULT_var(Module *M) return prepare_global_in(M, jlRTLD_DEFAULT_var); } +typedef struct { + jl_value_t *gcroot[2]; // GC roots for strings [f_name, f_lib] + + // Static name resolution (compile-time known) + const char *f_name; // static function name + const char *f_lib; // static library name + + // Dynamic name resolution (simple runtime expressions) + jl_value_t *f_name_expr; // expression for function name + jl_value_t *f_lib_expr; // expression for library name + + // Runtime pointer + Value *jl_ptr; // callable pointer expression result +} native_sym_arg_t; // Find or create the GVs for the library and symbol lookup. -// Return `runtime_lib` (whether the library name is a string) +// Return `runtime_lib` (whether the library name is a string) if it returns `lib`. // The `lib` and `sym` GV returned may not be in the current module. -static bool runtime_sym_gvs(jl_codectx_t &ctx, const char *f_lib, const char *f_name, +static bool runtime_sym_gvs(jl_codectx_t &ctx, const native_sym_arg_t &symarg, GlobalVariable *&lib, GlobalVariable *&sym) { + const auto &f_lib = symarg.f_lib; + const auto &f_name = symarg.f_name; + // If f_name isn't constant or f_lib_expr is present but not present, + // emit a local cache for sym, but do not cache lib + if (!((f_lib || symarg.f_lib_expr == NULL) && f_name)) { + std::string name = "dynccall_"; + name += std::to_string(jl_atomic_fetch_add_relaxed(&globalUniqueGeneratedNames, 1)); + Module *M = jl_Module; + auto T_pvoidfunc = getPointerTy(M->getContext()); + lib = nullptr; + sym = new GlobalVariable(*M, T_pvoidfunc, false, + GlobalVariable::InternalLinkage, + Constant::getNullValue(T_pvoidfunc), name); + return false; + } + auto M = &ctx.emission_context.shared_module(); bool runtime_lib = false; GlobalVariable *libptrgv; @@ -119,19 +149,22 @@ static bool runtime_sym_gvs(jl_codectx_t &ctx, const char *f_lib, const char *f_ static Value *runtime_sym_lookup( jl_codegen_params_t &emission_context, IRBuilder<> &irbuilder, - jl_codectx_t *ctx, - PointerType *funcptype, const char *f_lib, jl_value_t *lib_expr, - const char *f_name, Function *f, + jl_codectx_t *pctx, + const native_sym_arg_t &symarg, Function *f, GlobalVariable *libptrgv, GlobalVariable *llvmgv, bool runtime_lib) { ++RuntimeSymLookups; - // in pseudo-code, this function emits the following: + // in pseudo-code, this function emits the following if libptrgv is set: // global HMODULE *libptrgv // global void **llvmgv - // if (*llvmgv == NULL) { + // if (*llvmgv == NULL) // *llvmgv = jl_load_and_lookup(f_lib, f_name, libptrgv); - // } + // return (*llvmgv) + // otherwise it emits: + // global void **llvmgv + // if (*llvmgv == NULL) + // *llvmgv = jl_lazy_load_and_lookup(f_lib_expr, f_name_expr); // return (*llvmgv) auto T_pvoidfunc = getPointerTy(irbuilder.getContext()); BasicBlock *enter_bb = irbuilder.GetInsertBlock(); @@ -139,7 +172,6 @@ static Value *runtime_sym_lookup( BasicBlock *ccall_bb = BasicBlock::Create(irbuilder.getContext(), "ccall"); Constant *initnul = ConstantPointerNull::get(T_pvoidfunc); LoadInst *llvmf_orig = irbuilder.CreateAlignedLoad(T_pvoidfunc, llvmgv, Align(sizeof(void*))); - setName(emission_context, llvmf_orig, f_name + StringRef(".cached")); // This in principle needs a consume ordering so that load from // this pointer sees a valid value. However, this is not supported by // LLVM (or agreed on in the C/C++ standard FWIW) and should be @@ -159,26 +191,42 @@ static Value *runtime_sym_lookup( dlsym_lookup->insertInto(f); irbuilder.SetInsertPoint(dlsym_lookup); Instruction *llvmf; - Value *nameval = stringConstPtr(emission_context, irbuilder, f_name); - if (lib_expr) { - jl_cgval_t libval = emit_expr(*ctx, lib_expr); - llvmf = irbuilder.CreateCall(prepare_call_in(jl_builderModule(irbuilder), jllazydlsym_func), - { boxed(*ctx, libval), nameval }); - } - else { + if (libptrgv) { + // Call jl_load_and_lookup + assert(symarg.f_name); Value *libname; - if (runtime_lib) { - libname = stringConstPtr(emission_context, irbuilder, f_lib); - } - else { + if (runtime_lib) + libname = stringConstPtr(emission_context, irbuilder, symarg.f_lib); + else // f_lib is actually one of the special sentinel values - libname = ConstantExpr::getIntToPtr(ConstantInt::get(emission_context.DL.getIntPtrType(irbuilder.getContext()), (uintptr_t)f_lib), getPointerTy(irbuilder.getContext())); - } + libname = ConstantExpr::getIntToPtr(ConstantInt::get(emission_context.DL.getIntPtrType(irbuilder.getContext()), (uintptr_t)symarg.f_lib), getPointerTy(irbuilder.getContext())); + Value *nameval = stringConstPtr(emission_context, irbuilder, symarg.f_name); auto lookup = irbuilder.CreateCall(prepare_call_in(jl_builderModule(irbuilder), jldlsym_func), { libname, nameval, libptrgv }); llvmf = lookup; + setName(emission_context, llvmf, symarg.f_name + StringRef(".found")); + } + else { + // Call jl_lazy_load_and_lookup + assert(pctx); + jl_codectx_t &ctx = *pctx; + Value *fname_val; + if (symarg.f_name) + fname_val = track_pjlvalue(ctx, literal_pointer_val(ctx, (jl_value_t*)jl_symbol(symarg.f_name))); + else + fname_val = boxed(ctx, emit_expr(ctx, symarg.f_name_expr)); + Value *lib_val; + if (symarg.f_lib) + lib_val = track_pjlvalue(ctx, literal_pointer_val(ctx, (jl_value_t*)jl_symbol(symarg.f_lib))); + else if (symarg.f_lib_expr) + // n.b. f_lib_expr is required to be something simple here (from + // resolve_definition_effects validation) such as a globalref or a + // quote node for example, not a general expression + lib_val = boxed(ctx, emit_expr(ctx, symarg.f_lib_expr)); + else + lib_val = ConstantPointerNull::get(ctx.types().T_prjlvalue); + llvmf = irbuilder.CreateCall(prepare_call(jllazydlsym_func), {lib_val, fname_val}); } - setName(emission_context, llvmf, f_name + StringRef(".found")); StoreInst *store = irbuilder.CreateAlignedStore(llvmf, llvmgv, Align(sizeof(void*))); store->setAtomic(AtomicOrdering::Release); irbuilder.CreateBr(ccall_bb); @@ -188,38 +236,21 @@ static Value *runtime_sym_lookup( PHINode *p = irbuilder.CreatePHI(T_pvoidfunc, 2); p->addIncoming(llvmf_orig, enter_bb); p->addIncoming(llvmf, llvmf->getParent()); - setName(emission_context, p, f_name); return p; } static Value *runtime_sym_lookup( jl_codectx_t &ctx, - PointerType *funcptype, const char *f_lib, jl_value_t *lib_expr, - const char *f_name, Function *f) + const native_sym_arg_t &symarg, Function *f) { - auto T_pvoidfunc = getPointerTy(ctx.builder.getContext()); GlobalVariable *libptrgv; GlobalVariable *llvmgv; - bool runtime_lib; - if (lib_expr) { - // for computed library names, generate a global variable to cache the function - // pointer just for this call site. - runtime_lib = true; - libptrgv = NULL; - std::string gvname = "libname_"; - gvname += f_name; - gvname += "_"; - gvname += std::to_string(jl_atomic_fetch_add_relaxed(&globalUniqueGeneratedNames, 1)); - llvmgv = new GlobalVariable(*jl_Module, T_pvoidfunc, false, - GlobalVariable::ExternalLinkage, - Constant::getNullValue(T_pvoidfunc), gvname); - } - else { - runtime_lib = runtime_sym_gvs(ctx, f_lib, f_name, libptrgv, llvmgv); + bool runtime_lib = runtime_sym_gvs(ctx, symarg, libptrgv, llvmgv); + if (libptrgv) { libptrgv = prepare_global_in(jl_Module, libptrgv); + llvmgv = prepare_global_in(jl_Module, llvmgv); } - llvmgv = prepare_global_in(jl_Module, llvmgv); - return runtime_sym_lookup(ctx.emission_context, ctx.builder, &ctx, funcptype, f_lib, lib_expr, f_name, f, libptrgv, llvmgv, runtime_lib); + return runtime_sym_lookup(ctx.emission_context, ctx.builder, &ctx, symarg, f, libptrgv, llvmgv, runtime_lib); } // Emit a "PLT" entry that will be lazily initialized @@ -227,17 +258,24 @@ static Value *runtime_sym_lookup( static GlobalVariable *emit_plt_thunk( jl_codectx_t &ctx, FunctionType *functype, const AttributeList &attrs, - CallingConv::ID cc, const char *f_lib, const char *f_name, + CallingConv::ID cc, const native_sym_arg_t &symarg, GlobalVariable *libptrgv, GlobalVariable *llvmgv, bool runtime_lib) { ++PLTThunks; - auto M = &ctx.emission_context.shared_module(); - PointerType *funcptype = PointerType::get(functype, 0); - libptrgv = prepare_global_in(M, libptrgv); - llvmgv = prepare_global_in(M, llvmgv); + bool shared = libptrgv != nullptr; + assert(shared && "not yet supported by runtime_sym_lookup"); + Module *M = shared ? &ctx.emission_context.shared_module() : jl_Module; + if (shared) { + assert(symarg.f_name); + libptrgv = prepare_global_in(M, libptrgv); + llvmgv = prepare_global_in(M, llvmgv); + } std::string fname; - raw_string_ostream(fname) << "jlplt_" << f_name << "_" << jl_atomic_fetch_add_relaxed(&globalUniqueGeneratedNames, 1); + if (symarg.f_name) + raw_string_ostream(fname) << "jlplt_" << symarg.f_name << "_" << jl_atomic_fetch_add_relaxed(&globalUniqueGeneratedNames, 1); + else + raw_string_ostream(fname) << "jldynplt_" << jl_atomic_fetch_add_relaxed(&globalUniqueGeneratedNames, 1); Function *plt = Function::Create(functype, GlobalVariable::PrivateLinkage, fname, M); @@ -246,18 +284,19 @@ static GlobalVariable *emit_plt_thunk( plt->setCallingConv(cc); auto T_pvoidfunc = getPointerTy(M->getContext()); GlobalVariable *got = new GlobalVariable(*M, T_pvoidfunc, false, - GlobalVariable::ExternalLinkage, + shared ? GlobalVariable::ExternalLinkage : GlobalVariable::PrivateLinkage, plt, fname + "_got"); - if (runtime_lib) { - got->addAttribute("julia.libname", f_lib); - } else { - got->addAttribute("julia.libidx", std::to_string((uintptr_t) f_lib)); + if (shared) { + if (runtime_lib) + got->addAttribute("julia.libname", symarg.f_lib); + else + got->addAttribute("julia.libidx", std::to_string((uintptr_t) symarg.f_lib)); + got->addAttribute("julia.fname", symarg.f_name); } - got->addAttribute("julia.fname", f_name); BasicBlock *b0 = BasicBlock::Create(M->getContext(), "top", plt); IRBuilder<> irbuilder(b0); - Value *ptr = runtime_sym_lookup(ctx.emission_context, irbuilder, NULL, funcptype, f_lib, NULL, f_name, plt, libptrgv, + Value *ptr = runtime_sym_lookup(ctx.emission_context, irbuilder, NULL, symarg, plt, libptrgv, llvmgv, runtime_lib); StoreInst *store = irbuilder.CreateAlignedStore(ptr, got, Align(sizeof(void*))); store->setAtomic(AtomicOrdering::Release); @@ -302,7 +341,7 @@ static Value *emit_plt( jl_codectx_t &ctx, FunctionType *functype, const AttributeList &attrs, - CallingConv::ID cc, const char *f_lib, const char *f_name) + CallingConv::ID cc, const native_sym_arg_t &symarg) { ++PLT; // Don't do this for vararg functions so that the `musttail` is only @@ -310,18 +349,20 @@ static Value *emit_plt( assert(!functype->isVarArg()); GlobalVariable *libptrgv; GlobalVariable *llvmgv; - bool runtime_lib = runtime_sym_gvs(ctx, f_lib, f_name, libptrgv, llvmgv); + bool runtime_lib = runtime_sym_gvs(ctx, symarg, libptrgv, llvmgv); + if (!libptrgv) + return runtime_sym_lookup(ctx, symarg, ctx.f); auto &pltMap = ctx.emission_context.allPltMap[attrs]; auto key = std::make_tuple(llvmgv, functype, cc); GlobalVariable *&sharedgot = pltMap[key]; if (!sharedgot) { sharedgot = emit_plt_thunk(ctx, - functype, attrs, cc, f_lib, f_name, libptrgv, llvmgv, runtime_lib); + functype, attrs, cc, symarg, libptrgv, llvmgv, runtime_lib); } GlobalVariable *got = prepare_global_in(jl_Module, sharedgot); LoadInst *got_val = ctx.builder.CreateAlignedLoad(got->getValueType(), got, Align(sizeof(void*))); - setName(ctx.emission_context, got_val, f_name); + setName(ctx.emission_context, got_val, symarg.f_name); // See comment in `runtime_sym_lookup` above. This in principle needs a // consume ordering too. This is even less likely to cause issues though // since the only thing we do to this loaded pointer is to call it @@ -551,65 +592,44 @@ static Value *julia_to_native( return slot; } -typedef struct { - Value *jl_ptr; // if the argument is a run-time computed pointer - void (*fptr)(void); // if the argument is a constant pointer - const char *f_name; // if the symbol name is known - const char *f_lib; // if a library name is specified - jl_value_t *lib_expr; // expression to compute library path lazily - jl_value_t *gcroot; -} native_sym_arg_t; - -static inline const char *invalid_symbol_err_msg(bool ccall) -{ - return ccall ? - "ccall: first argument not a pointer or valid constant expression" : - "cglobal: first argument not a pointer or valid constant expression"; -} - // --- parse :sym or (:sym, :lib) argument into address info --- -static void interpret_symbol_arg(jl_codectx_t &ctx, native_sym_arg_t &out, jl_value_t *arg, bool ccall, bool llvmcall) +static void interpret_cglobal_symbol_arg(jl_codectx_t &ctx, native_sym_arg_t &out, jl_value_t *arg) { Value *&jl_ptr = out.jl_ptr; - void (*&fptr)(void) = out.fptr; const char *&f_name = out.f_name; const char *&f_lib = out.f_lib; - jl_value_t *ptr = static_eval(ctx, arg); if (ptr == NULL) { if (jl_is_expr(arg) && ((jl_expr_t*)arg)->head == jl_call_sym && jl_expr_nargs(arg) == 3 && jl_is_globalref(jl_exprarg(arg,0)) && jl_globalref_mod(jl_exprarg(arg,0)) == jl_core_module && jl_globalref_name(jl_exprarg(arg,0)) == jl_symbol("tuple")) { - // attempt to interpret a non-constant 2-tuple expression as (func_name, lib_name()), where - // `lib_name()` will be executed when first used. - jl_value_t *name_val = static_eval(ctx, jl_exprarg(arg,1)); - if (name_val && jl_is_symbol(name_val)) { - f_name = jl_symbol_name((jl_sym_t*)name_val); - out.lib_expr = jl_exprarg(arg, 2); - return; + // attempt to interpret a non-constant 2-tuple expression as (func_name, lib_name) + out.f_name_expr = jl_exprarg(arg, 1); + out.f_lib_expr = jl_exprarg(arg, 2); + jl_value_t *name_val = static_eval(ctx, out.f_name_expr); + out.gcroot[0] = name_val; + if (name_val) { + if (jl_is_symbol(name_val)) + f_name = jl_symbol_name((jl_sym_t*)name_val); + else if (jl_is_string(name_val)) + f_name = jl_string_data(name_val); } - else if (name_val && jl_is_string(name_val)) { - f_name = jl_string_data(name_val); - out.gcroot = name_val; - out.lib_expr = jl_exprarg(arg, 2); - return; + jl_value_t *lib_val = static_eval(ctx, out.f_lib_expr); + out.gcroot[1] = lib_val; + if (lib_val) { + if (jl_is_symbol(lib_val)) + f_lib = jl_symbol_name((jl_sym_t*)lib_val); + else if (jl_is_string(lib_val)) + f_lib = jl_string_data(lib_val); } } - jl_cgval_t arg1 = emit_expr(ctx, arg); - jl_value_t *ptr_ty = arg1.typ; - if (!jl_is_cpointer_type(ptr_ty)) { - if (!ccall) - return; - const char *errmsg = invalid_symbol_err_msg(ccall); - emit_cpointercheck(ctx, arg1, errmsg); + else { } - arg1 = update_julia_type(ctx, arg1, (jl_value_t*)jl_voidpointer_type); - jl_ptr = emit_unbox(ctx, ctx.types().T_ptr, arg1, (jl_value_t*)jl_voidpointer_type); } else { - out.gcroot = ptr; if (jl_is_tuple(ptr) && jl_nfields(ptr) == 1) { ptr = jl_fieldref(ptr, 0); + out.gcroot[0] = ptr; } if (jl_is_symbol(ptr)) @@ -620,23 +640,15 @@ static void interpret_symbol_arg(jl_codectx_t &ctx, native_sym_arg_t &out, jl_va if (f_name != NULL) { // just symbol, default to JuliaDLHandle // will look in process symbol table - if (!llvmcall) { - void *symaddr; - std::string iname("i"); - iname += f_name; - if (jl_dlsym(jl_libjulia_internal_handle, iname.c_str(), &symaddr, 0)) { - f_lib = JL_LIBJULIA_INTERNAL_DL_LIBNAME; - f_name = jl_symbol_name(jl_symbol(iname.c_str())); - } - else { - f_lib = jl_dlfind(f_name); - } - } + f_lib = jl_dlfind(f_name); + out.f_name_expr = jl_new_struct(jl_quotenode_type, ptr); + out.gcroot[0] = out.f_name_expr; } else if (jl_is_cpointer_type(jl_typeof(ptr))) { - fptr = *(void(**)(void))jl_data_ptr(ptr); + uint64_t fptr = (uintptr_t)*(void(**)(void))jl_data_ptr(ptr); + jl_ptr = ConstantExpr::getIntToPtr(ConstantInt::get(ctx.types().T_size, fptr), ctx.types().T_ptr); } - else if (jl_is_tuple(ptr) && jl_nfields(ptr) > 1) { + else if (jl_is_tuple(ptr) && jl_nfields(ptr) == 2) { jl_value_t *t0 = jl_fieldref(ptr, 0); if (jl_is_symbol(t0)) f_name = jl_symbol_name((jl_sym_t*)t0); @@ -648,11 +660,105 @@ static void interpret_symbol_arg(jl_codectx_t &ctx, native_sym_arg_t &out, jl_va f_lib = jl_symbol_name((jl_sym_t*)t1); else if (jl_is_string(t1)) f_lib = jl_string_data(t1); - else { - out.lib_expr = t1; + + out.f_name_expr = jl_new_struct(jl_quotenode_type, t0); + out.gcroot[0] = out.f_name_expr; + out.f_lib_expr = jl_new_struct(jl_quotenode_type, t1); + out.gcroot[1] = out.f_lib_expr; + } + } +} + +static void interpret_ccall_symbol_arg(jl_codectx_t &ctx, native_sym_arg_t &out, jl_value_t *arg) +{ + // Initialize all fields to safe defaults + out.f_name = nullptr; + out.f_lib = nullptr; + out.f_name_expr = nullptr; + out.f_lib_expr = nullptr; + out.jl_ptr = nullptr; + out.gcroot[0] = nullptr; + out.gcroot[1] = nullptr; + + // Check if this is a tuple (normalized by julia-syntax.scm) + if (jl_is_expr(arg) && ((jl_expr_t*)arg)->head == jl_symbol("tuple")) { + size_t nargs = jl_expr_nargs(arg); + jl_array_t *tuple_args = ((jl_expr_t*)arg)->args; + + if (nargs == 1) { + // Single element tuple: (func_name,) - use default library + jl_value_t *fname_arg = jl_array_ptr_ref(tuple_args, 0); + jl_value_t *fname_val = static_eval(ctx, fname_arg); + // Dynamic resolution - single function name expression, will use default library at runtime + out.f_name_expr = fname_arg; + + if (fname_val != nullptr) { + // Static resolution succeeded + out.gcroot[0] = fname_val; + if (jl_is_symbol(fname_val)) { + out.f_name = jl_symbol_name((jl_sym_t*)fname_val); + } + else if (jl_is_string(fname_val)) { + out.f_name = jl_string_data(fname_val); + } + } + } + else if (nargs == 2) { + // Two element tuple: (func_name, lib_name) + jl_value_t *fname_arg = jl_array_ptr_ref(tuple_args, 0); + jl_value_t *lib_arg = jl_array_ptr_ref(tuple_args, 1); + out.f_name_expr = fname_arg; + out.f_lib_expr = lib_arg; + + jl_value_t *fname_val = static_eval(ctx, fname_arg); + jl_value_t *lib_val = static_eval(ctx, lib_arg); + if (fname_val != nullptr) { + // Static resolution for both + out.gcroot[0] = fname_val; // Keep function name for GC + if (jl_is_symbol(fname_val)) { + out.f_name = jl_symbol_name((jl_sym_t*)fname_val); + } + else if (jl_is_string(fname_val)) { + out.f_name = jl_string_data(fname_val); + } + } + + if (lib_val != nullptr) { + out.gcroot[1] = lib_val; // Keep library name for GC + if (jl_is_symbol(lib_val)) { + out.f_lib = jl_symbol_name((jl_sym_t*)lib_val); + } + else if (jl_is_string(lib_val)) { + out.f_lib = jl_string_data(lib_val); + } } } } + else { + // Not a tuple - pointer expression + jl_cgval_t arg1 = emit_expr(ctx, arg); + jl_value_t *ptr_ty = arg1.typ; + if (!jl_is_cpointer_type(ptr_ty)) { + const char *errmsg = "ccall: first argument not a pointer or valid constant expression"; + emit_cpointercheck(ctx, arg1, errmsg); + } + arg1 = update_julia_type(ctx, arg1, (jl_value_t*)jl_voidpointer_type); + out.jl_ptr = emit_unbox(ctx, ctx.types().T_ptr, arg1, (jl_value_t*)jl_voidpointer_type); + } + + // Handle Julia internal symbol lookup for static function names + if (out.f_name != nullptr && out.f_lib_expr == nullptr) { + void *symaddr; + std::string iname("i"); + iname += out.f_name; + if (jl_dlsym(jl_libjulia_internal_handle, iname.c_str(), &symaddr, 0)) { + out.f_lib = JL_LIBJULIA_INTERNAL_DL_LIBNAME; + out.f_name = jl_symbol_name(jl_symbol(iname.c_str())); + } + else { + out.f_lib = jl_dlfind(out.f_name); + } + } } // --- code generator for cglobal --- @@ -666,7 +772,7 @@ static jl_cgval_t emit_cglobal(jl_codectx_t &ctx, jl_value_t **args, size_t narg jl_value_t *rt = NULL; Value *res; native_sym_arg_t sym = {}; - JL_GC_PUSH2(&rt, &sym.gcroot); + JL_GC_PUSH3(&rt, &sym.gcroot[0], &sym.gcroot[1]); if (nargs == 2) { rt = static_eval(ctx, args[2]); @@ -684,32 +790,25 @@ static jl_cgval_t emit_cglobal(jl_codectx_t &ctx, jl_value_t **args, size_t narg else { rt = (jl_value_t*)jl_voidpointer_type; } - Type *lrt = ctx.types().T_ptr; - assert(lrt == julia_type_to_llvm(ctx, rt)); - - interpret_symbol_arg(ctx, sym, args[1], /*ccall=*/false, false); - + interpret_cglobal_symbol_arg(ctx, sym, args[1]); if (sym.jl_ptr != NULL) { res = sym.jl_ptr; } - else if (sym.fptr != NULL) { - res = ConstantInt::get(lrt, (uint64_t)sym.fptr); + else if (sym.f_name_expr != NULL) { + res = runtime_sym_lookup(ctx, sym, ctx.f); } - else if (sym.f_name != NULL) { - if (sym.lib_expr) { - res = runtime_sym_lookup(ctx, getPointerTy(ctx.builder.getContext()), NULL, sym.lib_expr, sym.f_name, ctx.f); - } - else { - res = runtime_sym_lookup(ctx, getPointerTy(ctx.builder.getContext()), sym.f_lib, NULL, sym.f_name, ctx.f); - } - } else { + else { // Fall back to runtime intrinsic JL_GC_POP(); jl_cgval_t argv[2]; argv[0] = emit_expr(ctx, args[1]); if (nargs == 2) argv[1] = emit_expr(ctx, args[2]); - return emit_runtime_call(ctx, nargs == 1 ? JL_I::cglobal_auto : JL_I::cglobal, argv, nargs); + if (!jl_is_cpointer_type(argv[0].typ)) + return emit_runtime_call(ctx, nargs == 1 ? JL_I::cglobal_auto : JL_I::cglobal, argv, nargs); + argv[0] = update_julia_type(ctx, argv[0], (jl_value_t*)jl_voidpointer_type); + sym.jl_ptr = emit_unbox(ctx, ctx.types().T_ptr, argv[0], (jl_value_t*)jl_voidpointer_type); + res = sym.jl_ptr; } JL_GC_POP(); @@ -1436,33 +1535,17 @@ static jl_cgval_t emit_ccall(jl_codectx_t &ctx, jl_value_t **args, size_t nargs) } assert(jl_is_symbol(cc_sym)); native_sym_arg_t symarg = {}; - JL_GC_PUSH3(&rt, &at, &symarg.gcroot); + JL_GC_PUSH4(&rt, &at, &symarg.gcroot[0], &symarg.gcroot[1]); CallingConv::ID cc = CallingConv::C; bool llvmcall = false; std::tie(cc, llvmcall) = convert_cconv(cc_sym); - interpret_symbol_arg(ctx, symarg, args[1], /*ccall=*/true, llvmcall); - Value *&jl_ptr = symarg.jl_ptr; - void (*&fptr)(void) = symarg.fptr; + interpret_ccall_symbol_arg(ctx, symarg, args[1]); const char *&f_name = symarg.f_name; const char *&f_lib = symarg.f_lib; - if (f_name == NULL && fptr == NULL && jl_ptr == NULL) { - if (symarg.gcroot != NULL) { // static_eval(ctx, args[1]) could not be interpreted to a function pointer - const char *errmsg = invalid_symbol_err_msg(/*ccall=*/true); - jl_cgval_t arg1 = emit_expr(ctx, args[1]); - emit_type_error(ctx, arg1, literal_pointer_val(ctx, (jl_value_t *)jl_pointer_type), errmsg); - } else { - emit_error(ctx, "ccall: null function pointer"); - } - JL_GC_POP(); - return jl_cgval_t(); - } - - auto _is_libjulia_func = [&] (uintptr_t ptr, StringRef name) { - if ((uintptr_t)fptr == ptr) - return true; + auto _is_libjulia_func = [&f_lib, &f_name] (StringRef name) { if (f_lib) { if ((f_lib == JL_EXE_LIBNAME) || // preventing invalid pointer access (f_lib == JL_LIBJULIA_INTERNAL_DL_LIBNAME) || @@ -1480,7 +1563,7 @@ static jl_cgval_t emit_ccall(jl_codectx_t &ctx, jl_value_t **args, size_t nargs) } return f_name && f_name == name; }; -#define is_libjulia_func(name) _is_libjulia_func((uintptr_t)&(name), StringRef(XSTR(name))) +#define is_libjulia_func(name) _is_libjulia_func(StringRef(XSTR(name))) // emit arguments SmallVector argv(nccallargs); @@ -2075,20 +2158,16 @@ jl_cgval_t function_sig_t::emit_a_ccall( Value *llvmf; if (llvmcall) { ++EmittedLLVMCalls; - if (symarg.jl_ptr != NULL) { - emit_error(ctx, "llvmcall doesn't support dynamic pointers"); + if (symarg.f_name == NULL) { + // TODO: this should be checked/enforced a bit better (less dynamically) + emit_error(ctx, "llvmcall doesn't support dynamic names"); return jl_cgval_t(); } - else if (symarg.fptr != NULL) { - emit_error(ctx, "llvmcall doesn't support static pointers"); - return jl_cgval_t(); - } - else if (symarg.f_lib != NULL) { + else if (symarg.f_lib_expr != NULL) { emit_error(ctx, "llvmcall doesn't support dynamic libraries"); return jl_cgval_t(); } else { - assert(symarg.f_name != NULL); StringRef f_name(symarg.f_name); bool f_extern = f_name.consume_front("extern "); llvmf = NULL; @@ -2138,36 +2217,25 @@ jl_cgval_t function_sig_t::emit_a_ccall( null_pointer_check(ctx, symarg.jl_ptr, nullptr); llvmf = symarg.jl_ptr; } - else if (symarg.fptr != NULL) { - ++LiteralCCalls; - Type *funcptype = PointerType::getUnqual(functype->getContext()); - llvmf = literal_static_pointer_val((void*)(uintptr_t)symarg.fptr, funcptype); - setName(ctx.emission_context, llvmf, "ccall_fptr"); - } else if (!ctx.params->use_jlplt) { if ((symarg.f_lib && !((symarg.f_lib == JL_EXE_LIBNAME) || (symarg.f_lib == JL_LIBJULIA_INTERNAL_DL_LIBNAME) || - (symarg.f_lib == JL_LIBJULIA_DL_LIBNAME))) || symarg.lib_expr) { + (symarg.f_lib == JL_LIBJULIA_DL_LIBNAME))) || symarg.f_lib_expr) { + // n.b. this is not semantically valid, but use_jlplt=1 when semantic correctness is desired emit_error(ctx, "ccall: Had library expression, but symbol lookup was disabled"); } + if (symarg.f_name == nullptr) + emit_error(ctx, "ccall: Had name expression, but symbol lookup was disabled"); llvmf = jl_Module->getOrInsertFunction(symarg.f_name, functype).getCallee(); } else { - assert(symarg.f_name != NULL); - PointerType *funcptype = PointerType::get(functype, 0); - if (symarg.lib_expr) { - ++DeferredCCallLookups; - llvmf = runtime_sym_lookup(ctx, funcptype, NULL, symarg.lib_expr, symarg.f_name, ctx.f); - } - else { - ++DeferredCCallLookups; - // vararg requires musttail, - // but musttail is incompatible with noreturn. - if (functype->isVarArg()) - llvmf = runtime_sym_lookup(ctx, funcptype, symarg.f_lib, NULL, symarg.f_name, ctx.f); - else - llvmf = emit_plt(ctx, functype, attributes, cc, symarg.f_lib, symarg.f_name); - } + ++DeferredCCallLookups; + // vararg requires musttail, + // but musttail is incompatible with noreturn. + if (functype->isVarArg()) + llvmf = runtime_sym_lookup(ctx, symarg, ctx.f); + else + llvmf = emit_plt(ctx, functype, attributes, cc, symarg); } // Potentially we could add gc_uses to `gc-transition`, instead of emitting them separately as jl_roots diff --git a/src/codegen.cpp b/src/codegen.cpp index 2d9c94baee9b4..0950ce9a7abf3 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -1289,7 +1289,7 @@ static const auto jldlsym_func = new JuliaFunction<>{ static const auto jllazydlsym_func = new JuliaFunction<>{ XSTR(jl_lazy_load_and_lookup), [](LLVMContext &C) { return FunctionType::get(getPointerTy(C), - {JuliaType::get_prjlvalue_ty(C), getPointerTy(C)}, false); }, + {JuliaType::get_prjlvalue_ty(C), JuliaType::get_prjlvalue_ty(C)}, false); }, nullptr, }; static const auto jltypeassert_func = new JuliaFunction<>{ diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index 69869e3e923ec..a1422bc0a06cf 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -1088,6 +1088,20 @@ (values name params super)) ex) (error "invalid type signature"))) +;; normalize ccall first argument to tuple form with basic error checking +(define (normalize-ccall-name raw-name) + (cond + ;; Already a tuple - keep as-is, validation will happen in C + ((tuple-syntax? raw-name) + raw-name) + ;; Normalize atoms like strings and quoted values into 1-element tuple, since they aren't pointers + ((and (atom? raw-name) (not (symbol? raw-name))) + `(tuple ,raw-name)) + ((and (pair? raw-name) (quoted? raw-name)) + `(tuple ,raw-name)) + ;; Otherwise it is a pointer expression + (else (expand-forms raw-name)))) + ;; insert calls to convert() in ccall, and pull out expressions that might ;; need to be rooted before conversion. (define (lower-ccall name RT atypes args cconv nreq) @@ -1107,7 +1121,7 @@ (if (null? A) `(block ,.(reverse! stmts) - (foreigncall ,(expand-forms name) ,(expand-forms RT) (call (core svec) ,@(reverse! T)) + (foreigncall ,(normalize-ccall-name name) ,(expand-forms RT) (call (core svec) ,@(reverse! T)) ;; 0 or number of arguments before ... in definition ,(or nreq (if isseq (- (length atypes) 1) 0)) @@ -1151,7 +1165,7 @@ (error (string "invalid argument destructuring syntax \"" (deparse a) "\"")) a)) (define (transform-arg a) - (cond ((and (pair? a) (eq? (car a) 'tuple)) + (cond ((tuple-syntax? a) (let ((a2 (gensy))) (cons a2 `(local (= ,(check-lhs a) ,a2))))) ((or (and (decl? a) (length= a 3)) (kwarg? a)) @@ -1322,8 +1336,7 @@ (= ,vname ,tmp) ,blk))))))) ;; (a, b, c, ...) = rhs - ((and (pair? (cadar binds)) - (eq? (caadar binds) 'tuple)) + ((tuple-syntax? (cadar binds)) (let ((vars (lhs-vars (cadar binds)))) (loop (cdr binds) (let ((tmp (make-ssavalue))) @@ -1853,7 +1866,7 @@ (cons temp (append (cdr e) (list `(= ,temp ,newlhs)))) e)) (newlhs (or temp newlhs))) - (if (and (pair? lhs) (eq? (car lhs) 'tuple)) + (if (tuple-syntax? lhs) (let loop ((a (cdr newlhs)) (b (cdr lhs))) (if (pair? a) @@ -2483,7 +2496,7 @@ ((null? r) #f) ((vararg? (car r)) (null? (cdr r))) (else (sides-match? (cdr l) (cdr r))))) - (if (and (pair? x) (pair? lhss) (eq? (car x) 'tuple) (not (any assignment? (cdr x))) + (if (and (tuple-syntax? x) (pair? lhss) (not (any assignment? (cdr x))) (not (has-parameters? (cdr x))) (sides-match? lhss (cdr x))) ;; (a, b, ...) = (x, y, ...) @@ -2712,13 +2725,12 @@ (argtypes (cadr after-cconv)) (args (cddr after-cconv))) (begin - (if (not (and (pair? argtypes) - (eq? (car argtypes) 'tuple))) - (if (and (pair? RT) - (eq? (car RT) 'tuple)) + (if (not (tuple-syntax? argtypes)) + (if (tuple-syntax? RT) (error "ccall argument types must be a tuple; try \"(T,)\" and check if you specified a correct return type") (error "ccall argument types must be a tuple; try \"(T,)\""))) - (lower-ccall name RT (cdr argtypes) args + (lower-ccall name + RT (cdr argtypes) args (if have-cconv (if have-cconv-expr (cadr cconv) @@ -2763,6 +2775,20 @@ (map expand-forms e)))) (map expand-forms e))) + 'foreigncall + (lambda (e) + (if (not (length> e 5)) (error "too few arguments to foreigncall")) + (let* ((name (car (list-tail e 1))) + (RT (car (list-tail e 2))) + (atypes (car (list-tail e 3))) + (nreq (car (list-tail e 4))) + (cconv (car (list-tail e 5))) + (args-and-roots (list-tail e 6))) + (begin + ;; Return expanded foreigncall + `(foreigncall ,(normalize-ccall-name name) ,(expand-forms RT) ,(expand-forms atypes) + ,nreq ,cconv ,@(map expand-forms args-and-roots))))) + 'do (lambda (e) (let* ((call (cadr e)) @@ -4472,6 +4498,9 @@ f(x) = yt(x) (else #f)) #t)) +(define (tuple-syntax? fptr) + (and (pair? fptr) (eq? (car fptr) 'tuple))) + ;; this pass behaves like an interpreter on the given code. ;; to perform stateful operations, it calls `emit` to record that something ;; needs to be done. in value position, it returns an expression computing @@ -4739,10 +4768,10 @@ f(x) = yt(x) (let* ((args (cond ((eq? (car e) 'foreigncall) ;; NOTE: 2nd to 5th arguments of ccall must be left in place - ;; the 1st should be compiled if an atom. - (append (if (atom-or-not-tuple-call? (cadr e)) - (compile-args (list (cadr e)) break-labels) - (list (cadr e))) + ;; the 1st should be compiled unless it is a syntactic tuple from earlier + (append (if (tuple-syntax? (cadr e)) + (list (cadr e)) + (compile-args (list (cadr e)) break-labels)) (list-head (cddr e) 4) (compile-args (list-tail e 6) break-labels))) ;; NOTE: arguments of cfunction must be left in place @@ -5393,8 +5422,8 @@ f(x) = yt(x) (let ((e (cons (car e) (map renumber-stuff (cdr e))))) (if (and (eq? (car e) 'foreigncall) - (tuple-call? (cadr e)) - (expr-contains-p (lambda (x) (or (ssavalue? x) (slot? x))) (cadr e))) + (tuple-syntax? (cadr e)) + (expr-contains-p (lambda (x) (or (ssavalue? x) (slot? x))) (cadr e))) ;; TODO: use allow-list here (error "ccall function name and library expression cannot reference local variables")) e)))) (let ((body (renumber-stuff (lam:body lam))) diff --git a/src/julia_internal.h b/src/julia_internal.h index 4bf6acd17f4c8..f20aa09205b85 100644 --- a/src/julia_internal.h +++ b/src/julia_internal.h @@ -925,6 +925,7 @@ jl_value_t *jl_interpret_toplevel_expr_in(jl_module_t *m, jl_value_t *e, JL_DLLEXPORT int jl_is_toplevel_only_expr(jl_value_t *e) JL_NOTSAFEPOINT; jl_value_t *jl_call_scm_on_ast_and_loc(const char *funcname, jl_value_t *expr, jl_module_t *inmodule, const char *file, int line); +int jl_isa_ast_node(jl_value_t *e) JL_NOTSAFEPOINT; JL_DLLEXPORT jl_value_t *jl_method_lookup_by_tt(jl_tupletype_t *tt, size_t world, jl_value_t *_mt); JL_DLLEXPORT jl_method_instance_t *jl_method_lookup(jl_value_t **args, size_t nargs, size_t world); @@ -1636,7 +1637,7 @@ JL_DLLEXPORT void *jl_get_library_(const char *f_lib, int throw_err); void *jl_find_dynamic_library_by_addr(void *symbol, int throw_err); #define jl_get_library(f_lib) jl_get_library_(f_lib, 1) JL_DLLEXPORT void *jl_load_and_lookup(const char *f_lib, const char *f_name, _Atomic(void*) *hnd); -JL_DLLEXPORT void *jl_lazy_load_and_lookup(jl_value_t *lib_val, const char *f_name); +JL_DLLEXPORT void *jl_lazy_load_and_lookup(jl_value_t *lib_val, jl_value_t *f_name); JL_DLLEXPORT jl_value_t *jl_get_cfunction_trampoline( jl_value_t *fobj, jl_datatype_t *result, htable_t *cache, jl_svec_t *fill, void *(*init_trampoline)(void *tramp, void **nval), diff --git a/src/method.c b/src/method.c index 46a4f5bc4d56c..f7bfe67e7d4cc 100644 --- a/src/method.c +++ b/src/method.c @@ -79,7 +79,7 @@ JL_DLLEXPORT void jl_scan_method_source_now(jl_method_t *m, jl_value_t *src) // Resolve references to non-locally-defined variables to become references to global // variables in `module` (unless the rvalue is one of the type parameters in `sparam_vals`). static jl_value_t *resolve_definition_effects(jl_value_t *expr, jl_module_t *module, jl_svec_t *sparam_vals, jl_value_t *binding_edge, - int binding_effects, int eager_resolve) + int binding_effects) { if (jl_is_symbol(expr)) { jl_errorf("Found raw symbol %s in code returned from lowering. Expected all symbols to have been resolved to GlobalRef or slots.", @@ -104,10 +104,10 @@ static jl_value_t *resolve_definition_effects(jl_value_t *expr, jl_module_t *mod } // These exprs are not fully linearized if (e->head == jl_assign_sym) { - jl_exprargset(e, 1, resolve_definition_effects(jl_exprarg(e, 1), module, sparam_vals, binding_edge, binding_effects, eager_resolve)); + jl_exprargset(e, 1, resolve_definition_effects(jl_exprarg(e, 1), module, sparam_vals, binding_edge, binding_effects)); return expr; } else if (e->head == jl_new_opaque_closure_sym) { - jl_exprargset(e, 4, resolve_definition_effects(jl_exprarg(e, 4), module, sparam_vals, binding_edge, binding_effects, eager_resolve)); + jl_exprargset(e, 4, resolve_definition_effects(jl_exprarg(e, 4), module, sparam_vals, binding_edge, binding_effects)); return expr; } size_t nargs = jl_array_nrows(e->args); @@ -175,6 +175,54 @@ static jl_value_t *resolve_definition_effects(jl_value_t *expr, jl_module_t *mod if (e->head == jl_foreigncall_sym) { JL_NARGSV(ccall method definition, 5); // (fptr, rt, at, nreq, (cc, effects, gc_safe)) jl_task_t *ct = jl_current_task; + jl_value_t *fptr = jl_exprarg(e, 0); + // Handle dot expressions in tuple arguments for ccall by converting to GlobalRef eagerly + if (jl_is_expr(fptr) && ((jl_expr_t*)fptr)->head == jl_symbol("tuple")) { + jl_expr_t *tuple_expr = (jl_expr_t*)fptr; + size_t nargs_tuple = jl_expr_nargs(tuple_expr); + if (nargs_tuple == 0) + jl_error("ccall function name cannot be empty tuple"); + if (nargs_tuple > 2) + jl_error("ccall function name tuple can have at most 2 elements"); + // Validate tuple elements are not more complicated than inference/codegen can safely handle + for (size_t i = 0; i < nargs_tuple; i++) { + jl_value_t *arg = jl_exprarg(tuple_expr, i); + // Handle dot expressions by converting to a GlobalRef + if (jl_is_expr(arg) && ((jl_expr_t*)arg)->head == jl_dot_sym) { + jl_expr_t *dot_expr = (jl_expr_t*)arg; + if (jl_expr_nargs(dot_expr) != 2) + jl_error("ccall function name: invalid dot expression"); + jl_value_t *mod_expr = jl_exprarg(dot_expr, 0); + jl_value_t *sym_expr = jl_exprarg(dot_expr, 1); + if (!(jl_is_quotenode(sym_expr) && jl_is_symbol(jl_quotenode_value(sym_expr)))) + jl_type_error("ccall name dot expression", (jl_value_t*)jl_symbol_type, sym_expr); + JL_TRY { + // Evaluate the module expression + jl_value_t *mod_val = jl_toplevel_eval(module, mod_expr); + JL_TYPECHK(ccall name dot expression, module, mod_val); + JL_GC_PROMISE_ROOTED(mod_val); + // Create GlobalRef from evaluated module and quoted symbol + jl_sym_t *sym = (jl_sym_t*)jl_quotenode_value(sym_expr); + jl_value_t *globalref = jl_module_globalref((jl_module_t*)mod_val, sym); + jl_exprargset(tuple_expr, i, globalref); + } + JL_CATCH { + if (jl_typetagis(jl_current_exception(ct), jl_errorexception_type)) + jl_error("could not evaluate ccall function/library name (it might depend on a local variable)"); + else + jl_rethrow(); + } + } + else if (jl_is_quotenode(arg)) { + jl_value_t *quoted_val = jl_quotenode_value(arg); + if (!jl_is_symbol(quoted_val) && !jl_is_string(quoted_val)) + jl_type_error("ccall function name", (jl_value_t*)jl_symbol_type, quoted_val); + } + else if (!jl_is_globalref(arg) && jl_isa_ast_node(arg)) { + jl_type_error("ccall function name", (jl_value_t*)jl_symbol_type, arg); + } + } + } jl_value_t *rt = jl_exprarg(e, 1); jl_value_t *at = jl_exprarg(e, 2); if (!jl_is_type(rt)) { @@ -251,7 +299,7 @@ JL_DLLEXPORT void jl_resolve_definition_effects_in_ir(jl_array_t *stmts, jl_modu size_t i, l = jl_array_nrows(stmts); for (i = 0; i < l; i++) { jl_value_t *stmt = jl_array_ptr_ref(stmts, i); - jl_array_ptr_set(stmts, i, resolve_definition_effects(stmt, m, sparam_vals, binding_edge, binding_effects, 0)); + jl_array_ptr_set(stmts, i, resolve_definition_effects(stmt, m, sparam_vals, binding_edge, binding_effects)); } } @@ -952,7 +1000,7 @@ JL_DLLEXPORT void jl_method_set_source(jl_method_t *m, jl_code_info_t *src) } } else { - st = resolve_definition_effects(st, m->module, sparam_vars, (jl_value_t*)m, 1, 0); + st = resolve_definition_effects(st, m->module, sparam_vars, (jl_value_t*)m, 1); } jl_array_ptr_set(copy, i, st); } diff --git a/src/runtime_ccall.cpp b/src/runtime_ccall.cpp index a653b027a861a..9349920bd500b 100644 --- a/src/runtime_ccall.cpp +++ b/src/runtime_ccall.cpp @@ -64,22 +64,35 @@ void *jl_load_and_lookup(const char *f_lib, const char *f_name, _Atomic(void*) * // jl_load_and_lookup, but with library computed at run time on first call extern "C" JL_DLLEXPORT -void *jl_lazy_load_and_lookup(jl_value_t *lib_val, const char *f_name) +void *jl_lazy_load_and_lookup(jl_value_t *lib_val, jl_value_t *f_name) { void *lib_ptr; + const char *fname_str; + + if (jl_is_symbol(f_name)) + fname_str = jl_symbol_name((jl_sym_t*)f_name); + else if (jl_is_string(f_name)) + fname_str = jl_string_data(f_name); + else + jl_type_error("ccall function name", (jl_value_t*)jl_symbol_type, f_name); + + if (lib_val) { + if (jl_is_symbol(lib_val)) + lib_ptr = jl_get_library(jl_symbol_name((jl_sym_t*)lib_val)); + else if (jl_is_string(lib_val)) + lib_ptr = jl_get_library(jl_string_data(lib_val)); + else if (jl_libdl_dlopen_func != NULL) { + lib_ptr = jl_unbox_voidpointer(jl_apply_generic(jl_libdl_dlopen_func, &lib_val, 1)); + } else + jl_type_error("ccall", (jl_value_t*)jl_symbol_type, lib_val); + } + else { + // If the user didn't supply a library name, try to find it now from the runtime value of f_name + lib_ptr = jl_get_library(jl_dlfind(fname_str)); + } - if (jl_is_symbol(lib_val)) - lib_ptr = jl_get_library(jl_symbol_name((jl_sym_t*)lib_val)); - else if (jl_is_string(lib_val)) - lib_ptr = jl_get_library(jl_string_data(lib_val)); - else if (jl_libdl_dlopen_func != NULL) { - // Call `dlopen(lib_val)`; this is the correct path for the `LazyLibrary` case, - // but it also takes any other value, and so we define `dlopen(x::Any) = throw(TypeError(...))`. - lib_ptr = jl_unbox_voidpointer(jl_apply_generic(jl_libdl_dlopen_func, &lib_val, 1)); - } else - jl_type_error("ccall", (jl_value_t*)jl_symbol_type, lib_val); void *ptr; - jl_dlsym(lib_ptr, f_name, &ptr, 1); + jl_dlsym(lib_ptr, fname_str, &ptr, 1); return ptr; } diff --git a/test/ccall.jl b/test/ccall.jl index f193d16fc09e2..a4a434914438c 100644 --- a/test/ccall.jl +++ b/test/ccall.jl @@ -1537,6 +1537,14 @@ fn45187() = nothing @test Expr(:error, "only the trailing ccall argument type should have \"...\"") == Meta.lower(@__MODULE__, :(ccall(:fn, A, (A, B..., C...), a, x, y, z))) @test Expr(:error, "more types than arguments for ccall") == Meta.lower(@__MODULE__, :(ccall(:fn, A, (B, C...), ))) +# test for ccall first argument tuple validation errors +@test_throws "ccall function name cannot be empty tuple" eval(:(f() = ccall((), A, (), ))) +@test_throws "ccall function name tuple can have at most 2 elements" eval(:(f() = ccall((:a, :b, :c), A, (), ))) +@test_throws "ccall function name tuple can have at most 2 elements" eval(:(f() = ccall((:a, :b, :c, :d), A, (), ))) +@test_throws TypeError eval(:(f() = ccall((1 + 2,), A, (), ))) +@test_throws TypeError eval(:(f() = ccall((:a, 1 + 2), A, (), ))) +@test_throws TypeError eval(:(ccall_lazy_lib_name(x) = ccall((:testUcharX, compute_lib_name()), Int32, (UInt8,), x % UInt8))) + # cfunction on non-function singleton struct CallableSingleton end @@ -1751,18 +1759,16 @@ using Base: ccall_macro_parse, ccall_macro_lower end @testset "ensure the base-case of @ccall works, including library name and pointer interpolation" begin - call = ccall_macro_lower(:ccall, ccall_macro_parse( :( libstring.func( + ccallmacro = ccall_macro_lower(:ccall, ccall_macro_parse( :( libstring.func( str::Cstring, num1::Cint, num2::Cint )::Cstring))...) - @test call == Base.remove_linenums!( - quote - ccall($(Expr(:escape, :((:func, libstring)))), $(Expr(:cconv, (:ccall, UInt16(0), false), 0)), $(Expr(:escape, :Cstring)), ($(Expr(:escape, :Cstring)), $(Expr(:escape, :Cint)), $(Expr(:escape, :Cint))), $(Expr(:escape, :str)), $(Expr(:escape, :num1)), $(Expr(:escape, :num2))) - end) + ccallfunction = :(ccall($(Expr(:escape, :((:func, libstring)))), $(Expr(:cconv, (:ccall, UInt16(0), false), 0)), $(Expr(:escape, :Cstring)), ($(Expr(:escape, :Cstring)), $(Expr(:escape, :Cint)), $(Expr(:escape, :Cint))), $(Expr(:escape, :str)), $(Expr(:escape, :num1)), $(Expr(:escape, :num2)))) + @test ccallmacro == ccallfunction local fptr = :x - @test_throws ArgumentError("interpolated function `fptr` was not a Ptr{Cvoid}, but Symbol") @ccall $fptr()::Cvoid + @test_throws TypeError @ccall $fptr()::Cvoid end @testset "check error paths" begin @@ -1777,7 +1783,7 @@ end # no required args on varargs call @test_throws ArgumentError("C ABI prohibits vararg without one required argument") ccall_macro_parse(:( foo(; x::Cint)::Cint )) # not a function pointer - @test_throws ArgumentError("interpolated function `PROGRAM_FILE` was not a Ptr{Cvoid}, but String") @ccall $PROGRAM_FILE("foo"::Cstring)::Cvoid + @test_throws TypeError @ccall $PROGRAM_FILE("foo"::Cstring)::Cvoid end @testset "check error path for @cfunction" begin @@ -1834,10 +1840,6 @@ end end # issue #36458 -compute_lib_name() = "libcc" * "alltest" -ccall_lazy_lib_name(x) = ccall((:testUcharX, compute_lib_name()), Int32, (UInt8,), x % UInt8) -@test ccall_lazy_lib_name(0) == 0 -@test ccall_lazy_lib_name(3) == 1 ccall_with_undefined_lib() = ccall((:time, xx_nOt_DeFiNeD_xx), Cint, (Ptr{Cvoid},), C_NULL) @test_throws UndefVarError(:xx_nOt_DeFiNeD_xx, @__MODULE__) ccall_with_undefined_lib()