Spec-Zone.ru › Nim 1

Интеграция Nim Backend

"Застой порождает ересь." — Неизвестный.

Введение

Документация Руководства пользователя Nim Compiler описывает типичный вызов компилятора, использующий команду compile или c, для преобразования файла .nim в один или несколько файлов .c, которые затем компилируются с помощью компилятора C платформы в статический двоичный файл. Однако существуют и другие команды для компиляции в C++, Objective-C или JavaScript. Этот документ пытается собрать воедино все варианты бэкенда и интерфейсов.

Компилятор Nim поддерживает в основном две семейства бэкендов: цели C, C++ и Objective-C, а также цель JavaScript. Цели C-подобных целей создают исходные файлы, которые могут быть скомпилированы в библиотеку или конечный исполняемый файл. Цель JavaScript может генерировать файл .js, который вы ссылаетесь из файла HTML, или создать самостоятельную программу Node.js.

Помимо генерации библиотек или автономных приложений, Nim предлагает двусторонний интерфейс с целями бэкенда с помощью общих и специфичных пragma.

Бэкенды

Цели C-подобных целей

Команды для компиляции в C, C++ или Objective-C:

compileToC, cc компилировать проект с генератором кода C
compileToCpp, cpp компилировать проект в код C++
compileToOC, objc компилировать проект в код Objective C

Наиболее существенное различие между этими командами заключается в том, что если вы посмотрите в каталог nimcache, вы найдете файлы .c, .cpp или .m, а в остальном все они будут генерировать нативный двоичный файл для вашего проекта. Это позволяет вам взять сгенерированный код и поместить его непосредственно в проект, используя любой из этих языков. Вот несколько типичных вызовов командной строки:

$ nim c hallo.nim
$ nim cpp hallo.nim
$ nim objc hallo.nim

Команды компилятора выбирают целевой бэкенд, но при необходимости вы можете указать дополнительные переключатели для кросс-компиляции, чтобы выбрать целевой процессор, операционную систему или команды компилятора/линкера.

Цель JavaScript

Nim также может генерировать код JavaScript через команду js.

Nim использует JavaScript 1.5, который поддерживается любым широко используемым браузером. Поскольку JavaScript не имеет портативного способа включить другой модуль, Nim просто генерирует большой .js файл.

Функции или модули, которые не поддерживаются платформой JavaScript, недоступны. Это включает:

  • ручное управление памятью (alloc, и т.д.)
  • приведение типов и другие небезопасные операции (cast оператор, zeroMem, и т.д.)
  • управление файлами
  • операции, специфичные для ОС
  • потоки, сопрограммы
  • некоторые модули стандартной библиотеки
  • правильная арифметика 64-битных целых чисел

Для компенсации в стандартной библиотеке есть модули, предназначенные для бэкенда JS, и в будущем будет предоставлена ​​большая поддержка (например, привязки Node.js для получения информации об ОС).

Для компиляции модуля Nim в файл .js используйте команду js; по умолчанию это файл .js, который должен быть указан в файле .html. Однако вы также можете запустить код с помощью nodejs (http://nodejs.org):

nim js -d:nodejs -r examples/hallo.nim

Интерфейс

Nim предлагает двусторонний интерфейс с целевым бэкендом. Это означает, что вы можете вызывать код бэкенда из Nim, а код Nim может вызываться кодом бэкенда. Обычно направление вызовов зависит от вашей архитектуры программного обеспечения (является ли Nim вашей основной программой или Nim предоставляет компонент?).

Код Nim вызывает бэкенд

Код Nim может взаимодействовать с бэкендом через интерфейс внешних функций, в основном через pragma importc. Пragma importc является общим способом предоставления символов бэкенда в Nim и доступен во всех целевых бэкендах (включая JavaScript). Бэкенды C++ или Objective-C имеют свои соответствующие ImportCpp и ImportObjC пragma для вызова методов из классов.

Всякий раз, когда вы используете любой из этих пragma, вам необходимо интегрировать нативный код в ваш конечный двоичный файл. В случае JavaScript это не проблема, тот же файл HTML, который содержит сгенерированный JavaScript, вероятно, предоставит другие функции JavaScript, которые вы импортируете с помощью importc.

Однако для C-подобных целей вам необходимо связать внешний код статически или динамически. Предпочтительный способ интеграции нативного кода — использование динамической компоновки, так как оно позволяет компилировать программы Nim без необходимости наличия соответствующих библиотек разработки. Это делается с помощью pragma dynlib для импорта, хотя с помощью модуля dynlib можно получить больший контроль.

Переключатель командной строки dynlibOverride позволяет избежать динамической компоновки, если вам необходимо вместо этого использовать статическую компоновку. Обёртки Nim, предназначенные для статической компоновки исходных файлов, могут использовать pragma compile, если таких файлов мало, или если проще включить их вместе с кодом Nim, чем использовать системную библиотеку. Библиотеки, установленные на хост-системе, можно связать с помощью pragma PassL.

Для обёртки нативного кода ознакомьтесь с инструментом c2nim, который помогает в процессе сканирования и преобразования заголовочных файлов в интерфейс Nim.

Пример вызова C

Создайте файл logic.c со следующим содержимым:

int addTwoIntegers(int a, int b)
{
  return a + b;
}

Создайте файл calculator.nim со следующим содержимым:

{.compile: "logic.c".}
proc addTwoIntegers(a, b: cint): cint {.importc.}

when isMainModule:
  echo addTwoIntegers(3, 7)

После создания этих двух файлов, вы можете запустить nim c -r calculator.nim, и компилятор Nim скомпилирует файл logic.c помимо calculator.nim и объединит их в исполняемый файл, который выведет 10 при выполнении. Другой способ статически связать файл C и получить тот же эффект — удалить строку с пragma compile и выполнить следующие типичные unix-команды:

$ gcc -c logic.c
$ ar rvs mylib.a logic.o
$ nim c --passL:mylib.a -r calculator.nim

Точно так же, как в этом примере мы передаём путь к библиотеке mylib.a (и мы также могли бы передать logic.o), мы могли бы передавать переключатели для связывания с любыми другими статическими библиотеками C.

Пример вызова JavaScript

Создайте файл host.html со следующим содержимым:

<html><body>
<script type="text/javascript">
function addTwoIntegers(a, b)
{
  return a + b;
}
</script>
<script type="text/javascript" src="calculator.js"></script>
</body></html>

Создайте файл calculator.nim со следующим содержимым (или используйте тот, что из предыдущего раздела):

proc addTwoIntegers(a, b: int): int {.importc.}

when isMainModule:
  echo addTwoIntegers(3, 7)

Скомпилируйте код Nim в JavaScript с помощью nim js -o:calculator.js calculator.nim и откройте host.html в браузере. Если браузер поддерживает JavaScript, вы должны увидеть значение 10 в консоли браузера. Используйте модуль dom для специфичных запросов и модификаций DOM или ознакомьтесь с karax для разработки приложений на основе браузера.

Код бэкенда вызывает Nim

Код бэкенда может взаимодействовать с кодом Nim, экспортированным через pragma exportc. Пragma exportc является общим способом предоставления символов Nim бэкендам. По умолчанию компилятор Nim будет изменять все символы Nim, чтобы избежать конфликтов имён, поэтому самое главное, что делает пragma exportc, — это сохранить имя символа Nim или, при необходимости, использовать альтернативное имя для бэкенда в случае, если правила символов не совпадают.

Цель JavaScript не имеет дополнительных соображений по интерфейсу, так как также имеет сборку мусора, но цели C требуют инициализации внутренних компонентов Nim, что делается вызовом функции NimMain. Кроме того, код C требует указания объявления для функций, иначе компилятор предположит определённые типы для возвращаемого значения и параметров, что, вероятно, приведёт к аварийному завершению работы программы во время выполнения.

Компилятор Nim может сгенерировать заголовочный файл C-интерфейса с помощью переключателя командной строки --header. Сгенерированный заголовочный файл будет содержать все экспортированные символы и процедуру NimMain, которую нужно вызывать перед любым другим кодом Nim.

Пример вызова Nim из C

Создайте файл fib.nim со следующим содержимым:

proc fib(a: cint): cint {.exportc.} =
  if a <= 2:
    result = 1
  else:
    result = fib(a - 1) + fib(a - 2)

Создайте файл maths.c со следующим содержимым:

#include "fib.h"
#include <stdio.h>

int main(void)
{
  NimMain();
  for (int f = 0; f < 10; f++)
    printf("Fib of %d is %d\n", f, fib(f));
  return 0;
}

Теперь вы можете выполнить следующие команды Unix, чтобы сначала сгенерировать исходные файлы C из кода Nim, а затем связать их в статический двоичный файл вместе с вашей основной C-программой:

$ nim c --noMain --noLinking --header:fib.h fib.nim
$ gcc -o m -I$HOME/.cache/nim/fib_d -Ipath/to/nim/lib $HOME/.cache/nim/fib_d/*.c maths.c

Первая команда запускает компилятор Nim с тремя специальными параметрами: избежать генерации функции main() в сгенерированных файлах, избежать связывания объектных файлов в конечный двоичный файл и явно сгенерировать заголовочный файл для интеграции C. Все сгенерированные файлы помещаются в каталог nimcache. Поэтому следующая команда компилирует исходный файл maths.c плюс все файлы .c из nimcache. Кроме этого пути, вам также необходимо указать компилятору C, где найти заголовочный файл Nim nimbase.h.

Вместо того, чтобы полагаться на генерацию отдельных файлов .c, вы также можете попросить компилятор Nim сгенерировать статически связанную библиотеку:

$ nim c --app:staticLib --noMain --header fib.nim
$ gcc -o m -Inimcache -Ipath/to/nim/lib libfib.nim.a maths.c

Компилятор Nim будет обрабатывать связывание исходных файлов, сгенерированных в каталоге nimcache, в статическую библиотеку libfib.nim.a, которую вы затем можете связать со своей C-программой. Обратите внимание, что эти команды являются общими и будут различаться для каждой системы. Например, в системах Linux вам, вероятно, также потребуется использовать -ldl для связывания необходимой функциональности dlopen.

Пример вызова Nim из JavaScript

Создайте файл mhost.html со следующим содержимым:

<html><body>
<script type="text/javascript" src="fib.js"></script>
<script type="text/javascript">
alert("Fib for 9 is " + fib(9));
</script>
</body></html>

Создайте файл fib.nim со следующим содержимым (или используйте тот, что из предыдущего раздела):

proc fib(a: cint): cint {.exportc.} =
  if a <= 2:
    result = 1
  else:
    result = fib(a - 1) + fib(a - 2)

Компилируйте код Nim в JavaScript с помощью nim js -o:fib.js fib.nim и откройте mhost.html в браузере. Если браузер поддерживает JavaScript, вы должны увидеть окно с сообщением, отображающим текст Fib for 9 is 34. Как уже упоминалось ранее, JavaScript не требует вызова инициализации NimMain или аналогичной функции, и вы можете напрямую вызывать экспортированную процедуру Nim.

Логика именования nimcache

Директория nimcache генерируется во время компиляции и будет содержать временные или окончательные файлы в зависимости от целевого бэкэнда. Имя по умолчанию для директории зависит от используемого бэкэнда и вашей ОС, но вы можете изменить его с помощью --nimcache переключателя компилятора.

Управление памятью

В предыдущих разделах появлялась функция NimMain(). Поскольку JavaScript уже предоставляет автоматическое управление памятью, вы можете свободно передавать объекты между двумя языками без проблем. В языках C и производных от него вам нужно быть осторожным с тем, что вы делаете и как вы делитесь памятью. Предыдущие примеры касались только простых скалярных значений, но передача строки Nim в C или чтение строки C в Nim уже требует понимания, кто контролирует что, чтобы избежать сбоев.

Строки и строки C

В руководстве упоминается, что строки Nim неявно преобразуются в cstrings, что обычно упрощает взаимодействие. Большинство функций C, принимающих строку Nim, преобразованную в cstring, скорее всего, не будут нуждаться в хранении этой строки, и к моменту возврата функция не будет нужна. Однако в редких случаях, когда строка Nim должна быть сохранена и предоставлена бэкэнду C как cstring, вам нужно будет вручную предотвратить освобождение данных строки с помощью GC_ref и GC_unref.

Аналогичная ситуация возникает, когда код C вызывает код Nim, возвращающий cstring. Рассмотрим следующую процедуру:

proc gimme(): cstring {.exportc.} =
  result = "Hey there C code! " & $rand(100)

Поскольку сборщик мусора Nim не осведомлен о коде C, после завершения процедуры gimme он может освободить память, занимаемую cstring. Однако с практической точки зрения код C, вызывающий функцию gimme напрямую, сможет использовать ее, поскольку сборщик мусора Nim еще не успел выполниться. Это даёт достаточно времени, чтобы сделать копию для стороны C программы, так как вызов дальнейших процедур Nim может вызвать сборку мусора, делая ранее возвращенную строку мусорной. Или, возможно, вы сами вызываете сборку.

Пользовательские типы данных

Так же, как и строки, пользовательские типы данных, которые должны быть общими для Nim и бэкэнда, потребуют внимательного рассмотрения того, кто за что отвечает. Если вы хотите передать ссылку Nim коду C, вам необходимо использовать GC_ref, чтобы отметить ссылку как используемую, чтобы она не освобождалась. И для бэкэнда C вам потребуется предоставить процедуру GC_unref для очистки этой памяти, когда она больше не требуется.

Опять же, если вы обёртываете библиотеку, которая mallocs и frees структуры данных, вам нужно предоставить соответствующую функцию free для Nim, чтобы вы могли её очистить. И, конечно, после очистки вы должны избегать доступа к ней из Nim (или C). Как правило, структуры данных C имеют свои собственные malloc_structure и free_structure специфичные функции, поэтому их обертывание для стороны Nim должно быть достаточно.

Координация потоков

Когда вызывается функция NimMain() Nim инициализирует сборщик мусора для текущего потока, который обычно является основным потоком вашего приложения. Если ваш код C позже создаёт другой поток и вызывает код Nim, сборщик мусора не будет работать должным образом, и произойдёт сбой.

Пока вы не используете эмуляцию threadvar, Nim использует родные переменные потоков, из которых вы получаете новую версию при каждом создании потока. Вы можете затем прикрепить сборщик мусора к этому потоку следующим образом:

system.setupForeignThreadGc()

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

Перед завершением потока необходимо разорвать сборщик мусора потока, чтобы предотвратить утечку памяти, вызвав:

system.tearDownForeignThreadGc()

© 2006–2021 Andreas Rumpf
Licensed under the MIT License.
https://nim-lang.org/docs/backends.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API