На уровне языка, что такое `ccall`?

Я новичок в Julia, и я пытаюсь понять, на уровне языка, что ccall. На уровне синтаксиса он выглядит как нормальная функция, но он явно не ведет себя так же, как он принимает свои аргументы:

Обратите внимание, что кортеж типа аргумента должен быть буквальным кортежем, а не переменная или выражение, привязанное к кортежу.

Кроме того, если я оцениваю переменную, связанную с функцией в Julia REPL, я получаю что-то вроде

julia> max
max (generic function with 15 methods)

Но если я попытаюсь сделать то же самое с ccall:

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

Ясно, что ccall является специальным синтаксисом, но он также не является макросом (префикс @ и недопустимое использование макросов дает более конкретную ошибку). Итак, что это? Это что-то запеченное в языке, или что-то, что я мог бы определить с помощью какой-либо языковой конструкции, с которой я не знаком?

И если это какой-то запеченный кусок синтаксиса, почему было решено использовать нотацию функций, вместо того, чтобы реализовать его как макрос или создать более читаемый и отличный синтаксис?

Ответ 1

В текущем ночном (и, следовательно, предстоящем выпуске 0,6) большая часть специального поведения, которое вы наблюдаете была удалена (см. this pull-request). ccall больше не является зарезервированным словом, поэтому его можно использовать как имя функции или макроса.

Однако все еще есть небольшая странность: допускается функция 3 или 4 аргумента, называемая ccall, но на самом деле вызов такой функции даст ошибку о ccall argument types (другие числа аргументов в порядке). Причины идут прямо на ваш вопрос:

Итак, что это? Это что-то запеченное на языке

Да, ccall, хотя он больше не будет ключевым словом в 0.6, по-прежнему "испечен" на языке несколькими способами:

  • Форма выражения :ccall([four args...]) распознается и специально обработана во время понижения синтаксиса. Этот шаг понижения делает несколько вещей, включая обертывание аргументов при вызове unsafe_convert, что позволяет настраивать преобразование из объектов Julia в C-совместимые объекты; а также вытягивать аргументы, которые, возможно, должны быть внедрены, чтобы предотвратить сбор мусора ссылочного объекта во время ccall. (см. вывод code_lowered или попробуйте функцию expand, более подробную информацию о компиляторе здесь).
  • ccall требует расширенной обработки в бэкэнде генерации кода, в том числе: просмотр запрошенного имени функции в указанной разделяемой библиотеке и создание LLVM call, которая в конечном итоге переводится на машинный код, специфичный для платформы, компилятором LLVM Just-In-Time. (см. различные этапы с code_llvm и code_native).

И если это какой-то запеченный кусок синтаксиса, почему было решено использовать функции, но вместо ее реализации в качестве макроса или проектирование более читаемого и четкого синтаксиса?

По причинам, описанным выше, ccall требует специальной обработки, выглядит ли он как макрос или функция. В этой теме списка рассылки один из создателей Julia (Stefan Karpinski) прокомментировал, почему бы не сделать это макросом:

Я предполагаю, что мы могли бы переопределить его как макрос, но это действительно просто подтолкнуло бы магию дальше.

Что касается "более читаемого и четкого синтаксиса", возможно, это вопрос вкуса. Мне непонятно, почему предпочтительнее другой синтаксис (за исключением удобства синтаксиса синтаксиса синтаксиса LuaJIT/CFFI-стиля, из которых я являюсь поклонником). Мое единственное сильное личное пожелание ccall состояло бы в том, чтобы иметь аргументы и типы, введенные рядом (например, ccall((:foo, :libbar), Void, (x::Int, y::Float))), потому что работа с более длинными списками аргументов может быть неудобной. В 0.6 можно будет реализовать эту форму в виде макроса!

Ответ 2

В июле 0.5 и ранее. Это не функция, и она не является макросом. Это действительно что-то особенное, запеченное в языке. Это внутреннее. В julia 0.6 это изменяется

Это много похоже на макрос, чем на вызов функции. Но другими способами это не так - он не возвращает АСТ. Он вызывает функцию, и на достаточно низком уровне он похож на вызов функции julia.

История того, почему она выглядит так, как она есть, выше меня, вам нужно было услышать от одного из людей, которые работали над самым ранним кодом для языка. Сейчас это повсюду, и это одна из самых трудных вещей, которые нужно изменить, но не невозможно. Это вызвало бы 3-х летние байкинга: -P.

Мне нравится думать о ccall как о двух вещах.

  • Интерфейс внешних функций для C и других скомпилированных языков (например, Fortran, Rust, по-видимому, работают)
  • Способ доступа к необработанным кишкам языка "runtime".

Интерфейс внешних функций (FFI)

В большинстве случаев, когда вы используете ccall в пакете, вы хотите вызвать код, который находится в библиотеке компиляции. В этом смысле это C-Call, например, R-Call или Py-Call. Я думаю, mlewe/BlossomV.jl - хороший компактный пример. Для более интенсивного примера oxinabox/SLEEF.jl.

Как FFI, ему не нужно обмениваться памятью/процессом с julia - PyCall.jl does, RCall.jl и Matlab.jl этого не делают. Это не имеет значения, пока результат вернется. В этих случаях теоретически возможно заменить ccall на какой-то safe_ccall, который будет запускать вызываемую библиотеку в отдельном процессе и не будет segfault julia, если библиотека будет вызвана segfaulted. Но до сих пор никто не написал такой метод/пакет.

Использование ccall для FFI выполняется даже в Base, например, для доступа к MPFR для определения BigFloat. Но это не главная причина, по которой ccall используется в Base.

Доступ к кишкам языка.

ccall действительно то, что заставляет большую часть программы "делать что-то". Он используется во всех Base, чтобы вызвать функции из src.

Для этого ccall в основном запускает вызов функции на скомпилированном уровне, который сдвигает указатель инструкции непосредственно в скомпилированный код функции ccall ed. Как вызов функции, если бы все это было написано в слове C.

Вы можете видеть в base/threadingconstructs.jl ccall, используемый для управления работой с потоками, - который запускает код из src/threading.c.

Используется для отображения раздела диска в память. mmap.jl. - очевидно, не может быть сделано из другого процесса.

Он используется, чтобы сделать раздел кода неподдерживаемый

Используется вызов LibC, чтобы делать что-то вроде malloc для выделения памяти (хотя сейчас это в основном используется как часть FFI).

Есть трюки, которые вы можете сделать с ccall до #undef переменной после того, как она уже была назначена. ccall во многих отношениях является ключом мастера к языку.

Заключение

Я описал ccall здесь как две вещи: функцию FFI и основную часть языка "runtime". Эта двойственность не является реальной, и существует много перекрытий, таких как обработка файлов (это FFI?). По большому счету многие ожидают, что ccall будет использоваться с использованием FFI. Здесь ccall может быть просто функцией. Поведение, которое оно на самом деле имеет, исходит из его использования в качестве основной части языка - ссылки на код julia стандартной библиотеки в Base на код низкого уровня C от src. Позволяет очень прямое управление запуском процесса julia.