在语言层面,`ccall` 到底是什么?

At a language level, what exactly is `ccall`?

我是 Julia 的新手,我想在语言层面上理解 ccall 是什么。在语法层面上,它看起来像一个普通函数,但显然它在接受参数方面的行为方式不同:

Note that the argument type tuple must be a literal tuple, and not a tuple-valued variable or expression.

此外,如果我在 Julia REPL 中评估绑定到函数的变量,我会得到类似

的结果
julia> max
max (generic function with 15 methods)

但是如果我尝试对 ccall 做同样的事情:

julia> ccall
ERROR: syntax: invalid "ccall" syntax

显然,ccall 是一种特殊的语法,但它也不是宏(没有 @ 前缀,无效的宏使用会产生更具体的错误)。那么,它是什么?它是语言中固有的东西,还是我可以用我不熟悉的某种语言结构来定义自己的东西?

如果它是一些内置语法,为什么决定使用函数调用表示法,而不是将其实现为宏或设计更具可读性和独特性的语法?

在 Julia 0.5 及更早版本中。 它不是函数,也不是宏。 这确实是一种特殊的语言。 这是一个内在的。 In julia 0.6 this changes

它在很多方面更像是宏而不是函数调用。 但在其他方面它不是——它不是 return AST。 它确实调用了一个函数,并且在足够低的级别上它看起来类似于调用 julia 函数。

我不了解为什么它看起来像这样的历史,您需要听取一位从事该语言最早代码工作的人的意见。 现在它无处不在,是最难改变的事情之一——但并非不可能。 It would trigger up for 3 years of bikeshedding though :-P .

我喜欢将 ccall 视为两件事。

  • 外部函数接口,用于 C 和其他编译语言(例如 Fortran、Rust 显然有效)
  • 访问语言原始内容的方法 "runtime"。

外部函数接口 (FFI)

大多数时候,当一个人在一个包中使用 ccall 时,他想调用编译库中的一些代码。从这个意义上说,它是 C-Call,如 R-Call,或 Py-Call。 我认为 mlewe/BlossomV.jl 是一个很好的紧凑示例。 举个更激烈的例子 oxinabox/SLEEF.jl.

作为 FFI,它不必与 julia 共享内存 space/a 进程——PyCall.jl 会,RCall.jl 和 Matlab.jl 不会。 只要有结果回来就无所谓了。 在这些情况下,理论上可以用某种 safe_ccall 替换 ccall ,这将 运行 在单独的进程中调用的库,如果库被调用段错误,则不会对 julia 进行段错误。 但是到现在为止,no-one已经写了这样一篇method/package。

FFI 使用 ccall 甚至可以在 Base 中完成,例如访问 MPFR to define BigFloat。 但这不是ccall用于Base的主要原因。

进入语言的核心。

ccall 确实驱动了大部分程序 "doing a thing"。 它在整个 Base, to call the functions from src.

中使用

为此,ccall 基本上在编译级别触发函数调用,将指令指针直接转移到 ccalled 函数的编译代码中。就像调用一个函数一样,如果整个事情都是用 C 写的。

你可以在base/threadingconstructs.jl ccall being used to manage work on threads -- that triggers code from src/threading.c.

中看到

用于将一段磁盘映射到内存。 mmap.jl。 -- 显然不能从另一个进程完成。

用于制作一段代码non-intruptable

它用于调用 LibC 来执行 malloc 之类的事情来分配内存(尽管现在这主要用作 FFI 的一部分)。

在变量已经赋值后,您可以使用 ccall#undef 一些技巧。 ccall 在很多方面都是 "master" 语言的关键。

结论

我在这里将 ccall 描述为两件事,一个 FFI 函数和语言的核心部分 "runtime"。这种二元性不是真实的,并且有很多重叠,比如文件处理(是 FFI 吗?)。 许多人期望 ccall 具有的行为来自其 FFI 使用。 这里 ccall 可能只是一个函数。 它实际具有的行为来自于它作为语言的核心部分的使用——链接 Base to the low level C code from src 中标准库的 julia 代码。 允许非常直接地控制 julia 进程的 运行ning。

在当前的每晚(以及即将发布的 0.6 版本)中,您观察到的许多特殊行为 has been removed (see this pull-request)。 ccall 不再是保留字,因此可以用作函数名或宏名。

但是仍然有一点奇怪:允许定义一个名为 ccall 的 3 或 4 参数函数,但实际上调用这样的函数将给出一个关于 ccall argument types 的错误(其他数量的参数都可以)。原因直接针对您的问题:

So, what is it? Is it something baked into the language

是的,ccall,虽然它在 0.6 中不再是关键字,但仍然 "baked in" 在几个方面对语言来说:

  • :ccall([four args...]) 表达形式被识别并且 specially handled during syntax lowering. This lowering step does several things including wrapping arguments in a call to unsafe_convert, which allows for customized conversion from Julia objects to C-compatible objects; as well as pulling out arguments that might need to be rooted to prevent garbage collection of a referenced object during the ccall. (see code_lowered output, or try the expand function; more info on the compiler here).
  • ccall 需要 extensive handling in the code generation backend, including: look-up of the requested function name in the specified shared library, and generation of an LLVM call 指令——最终由 LLVM Just-In-Time 编译器翻译成 platform-specific 机器码。 (查看 code_llvmcode_native 的不同阶段)。

And if it is some baked-in piece of syntax, why was it decided to use function call notation, instead of implementing it as a macro or designing a more readable and distinct syntax?

由于上面详述的原因,ccall 需要特殊处理,无论它看起来像宏还是函数。在 this mailing list thread 中,Julia 的一位创建者 (Stefan Karpinski) 评论了为什么不将其设为宏:

I suppose we could reimplement it as a macro, but that would really just be pushing the magic further down.

至于 "a more readable and distinct syntax",也许这是一个品味问题。我不清楚为什么其他一些语法会更可取(除了 LuaJIT/CFFI-style 内联 C 语法解析的便利性,我是它的粉丝)。我对 ccall 的唯一强烈个人愿望是让参数和类型相邻输入(例如 ccall((:foo, :libbar), Void, (x::Int, y::Float))),因为使用更长的参数列表可能会很不方便。在 0.6 中,可以将此表单实现为宏!