Предисловие

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

  1. Прочтите документацию библиотеки, заголовки и, возможно, исходные файлы, чтобы понять, к чему вы будете привязаны.
  2. Напишите определение Crystal lib, указав fun, struct, enum и т. Д., Которые понадобятся вашим потребителям.
  3. Оборачивание этого lib в высокоуровневый API, который покажется вашим потребителям знакомым, безопасным и мощным - в лучшем случае тот факт, что это привязка C, будет всего лишь деталью реализации, о которой вашим пользователям даже не нужно будет думать !

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

Попутно я постараюсь объяснить важные основные концепции, но предполагаются некоторые рабочие знания или знакомство с C, указателями, типами и использованием компилятора C. О, и Кристалл тоже. Если вы не совсем уверены, рекомендуется быстро прочитать некоторые другие руководства по этим концепциям.

C

Интерфейс

Готовы прочитать код на C? Вот наша библиотека, к которой мы будем привязаны:

Давайте рассмотрим это.

#include <stddef.h>

Это стандартный заголовок C, который позволяет нам использовать полезные константы, такие как NULL. Нам не о чем беспокоиться в нашем случае использования.

struct operations

Это структура C, которая будет содержать наши обратные вызовы. Есть два:

  • void (*work) (int *, int, int); Указатель на функцию, который принимает ссылку на целое число и два целых числа. Ожидается, что функция, хранящаяся в этом члене, выполнит некоторую работу с двумя целыми числами и сохранит результат в ссылке.
  • void (*log) (int); Указатель на функцию, которая принимает целое число. Функция, хранящаяся в этом члене, просто вызывается с копией результата, полученного обратным вызовом work. В нашем случае мы будем использовать это, чтобы реализовать ведение журнала в STDOUT.

void call(const struct operations *ops, int a, int b)

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

Строкам, выполняющим обратные вызовы, предшествуют NULL проверки. Это делает каждый из обратных вызовов необязательным, поэтому, если мы не определим обработчик log в нашем коде, библиотека C ничего не сделает. Если бы этой проверки не было, он попытался бы получить доступ к недопустимому адресу памяти; kaboom, segfault!

Вот и все. На практике call будет запускаться из другого кода C после того, как мы передадим ему наши операции обратного вызова в другое место. Для наших целей мы будем запускать call из нашей привязки Crystal, чтобы упростить игру.

Пример использования C

Вот демонстрация реализации использования указанной выше библиотеки на C. В оставшейся части этой статьи предполагается, что test.c - это ранее рассмотренная библиотека C. Это в основном для полноты картины, и для тех из вас, кто уже имеет некоторый опыт работы с C, можно сделать то, о чем мы говорим, более конкретным:

Кристалл

Lib определение

Если вы пишете код, убедитесь, что вы скомпилировали нашу библиотеку в test.o файл! Кроме того, если вы вносите изменения в библиотеку C, не забудьте перекомпилировать ее. При запуске нашей программы Crystal она с радостью использует устаревший объектный файл!

Начнем с того, что напишем наше lib определение:

@[Link(ldflags: "#{__DIR__}/test.o")]

Это просто сообщает компилятору Crystal, что мы связываемся с test.o в текущем каталоге. На практике вам, вероятно, никогда не понадобится использовать ldflags таким образом, потому что библиотека, к которой вы привязываетесь, скорее всего, уже будет доступна в ваших системах PATH, поэтому достаточно будет @[Link("foo")].

struct Operations

Это наше кристаллическое представление структуры operations C. Конечный результат прост, но мне потребовалось некоторое время, чтобы понять это правильно.

Сначала я начал писать эту структуру, описывая ее так же, как это было бы в чистом кристалле:

Proc - это обычно то, как мы представляем и обрабатываем обратные вызовы от пользователей в пространстве Crystal. Но после некоторого изучения я выяснил, почему это было на самом деле неверно:

  1. Типы членов C struct в Crystal должны соответствовать тому, как структура в C фактически представлена ​​в памяти. Хотя определение C дает спецификацию аргумента, это не часть типа членов. В обоих случаях здесь мы имеем дело с указателями на функции, что означает, что оба эти поля должны быть Void*.
  2. Когда вы пишете lib, structs на самом деле не «привязаны» к соответствующей структуре C! Кристаллические структуры совместимы с C. Это означает, что вы можете назвать свой Crystal struct как угодно, дать ему любое количество членов с любым типом, и вы не обнаружите его неправильного до времени выполнения. Будь осторожен!

fun call(ops : Operations*, a : LibC::Int, b : LibC::Int)

Это наша привязка к функции call, которую мы будем использовать для тестирования наших обратных вызовов из кода Crystal. Здесь ничего особенного не происходит.

Пример использования lib

Теперь, когда у нас есть привязка к C lib, ее уже можно использовать! Давайте напишем небольшой пример, чтобы проверить, что это работает. Давайте воспроизведем предыдущий пример C:

Оно работает!

Вы заметите, что нам пришлось работать напрямую с указателем в нашем обратном вызове: result : Int32*. Это то, о чем мы хотим думать, когда пишем нашу оболочку.

Как мы можем избежать показа этого указателя пользователю и гарантировать, что он получит значение правильного типа?

Безопасная обертка

Мы можем сразу же начать перенос этой библиотеки в класс более высокого уровня, чтобы сразу получить некоторые преимущества:

И используйте его аналогичным образом:

Это становится намного более дружелюбным! Вот где мы сейчас находимся:

  • Знакомый синтаксис и семантика Crystal. То, что вы взаимодействуете с библиотекой C, почти не видно.
  • Компилятор заставляет пользователя передать правильный тип обратного вызова. Мы можем спать спокойно, зная, что обратный вызов пользователей нормален и должен работать должным образом с нашей библиотекой C.
  • Но мы все еще не можем предоставить пользователю Pointer.

Затем мы рассмотрим, как скрыть этот указатель и какие рефакторы нужно будет создать, чтобы сделать этот API безопасным.

Проблема: закрытие

Как выглядит идеальный интерфейс для этого? Наверное, примерно так:

Звучит хорошо. Теперь указатель исчез из нашего общедоступного интерфейса, и пользователь может вставить свое собственное поведение. Ограничение типа гарантирует, что мы вернем Int32, поэтому мы можем безопасно назначить его нашему указателю.

Но у нас проблемы:

Нехорошо! Что случилось? Давайте сделаем шаг назад и поговорим о Proc.

Рассмотрим следующий код:

Запуск этой программы напечатает что-то вроде:

Это демонстрирует несколько вещей:

  • a и b оба равны Proc(Int32). Но b помечен как closure. Всякий раз, когда внутри его тела делается ссылка на что-то извне локальной области действия процедуры, это «волшебным образом» становится отмеченным как закрытие.
  • Размер обоих этих типов составляет 16 байт. Это связано с тем, что Crystal Proc внутренне состоит из двух частей - 8 байтов, которые являются указателем на тело процедуры, и еще 8 байтов, которые относятся к контексту замыкания или, другими словами, к данным замыкания. Процесс разбивается на отдельные части с помощью вызовов proc.pointer и proc.closure_data.
  • C не знает об этих дополнительных данных; он получает только 8-байтовый указатель, который при вызове будет пытаться получить доступ к недопустимой памяти по адресу 0x0 (NULL) и segfaults.

Есть исключение из первого пункта, который дает решение нашей проблемы. Статические конструкции за пределами локальной области действия процедуры не помечают ее как закрытие. Это связано с тем, что расположение этих конструкций никогда не изменится в памяти, тогда как такие вещи, как локальные переменные, переменные экземпляра и т. Д., Будут находиться в разных местах и ​​могут перемещаться при изменении стека.

Решение: статические методы

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

Методы класса обеспечивают именно это:

Большой! Мы также можем передать этот интерфейс - который определят наши пользователи - нашему Test API, используя свободную переменную. Начнем переписывать наш класс:

Теперь мы можем использовать это так:

Здесь много всего происходит, поэтому давайте пройдемся по нескольким строкам:

def initialize(interface : T.class) forall T

Это наш новый конструктор, который принимает любой метакласс T и позволяет нам ссылаться на него позже в нашем методе. Когда компилятор создает наш экземпляр Test.new(MyOps), T заменяется на MyOps внутри initialize во время компиляции. Это означает, что если интерфейс в MyOps либо неправильно определен пользователем, либо неправильно используется нашей реализацией, мы получим ошибки времени компиляции вместо времени выполнения.

Например, если MyOps.handle_work вместо этого возвращает String, компиляция не удастся, потому что _61 _ (_ 62_) не сможет его получить!

Кроме того, если мы написали только def self.handle_work(a), мы получим ошибку времени компиляции из-за неправильного числа аргументов.

{% if T.class.has_method?(:handle_work) %}

Если вы помните, обратные вызовы нашей библиотеки C необязательны. Поэтому, если пользователь нашего класса Test не хочет реализовывать конкретный обратный вызов, эта проверка макроса позволяет им оставить его неопределенным в своем интерфейсе. Если бы у нас не было этой проверки, компилятор поднял бы вопрос о неопределенном методе. Это важная семантика для захвата, если бы библиотека C предоставляла нам обратные вызовы по умолчанию.

raise "work f is closure" if f.closure?

Это проверка работоспособности, которую, к сожалению, нельзя сделать во время компиляции, насколько мне известно. Я оставляю это в качестве дополнительной меры безопасности на случай, если Test будет взломан и обратные вызовы каким-то образом станут закрытием, или если был какой-то другой крайний случай, который я пропустил в системе типов Crystal.

Заключительный пример

Объединив все это вместе, мы получили действительно безопасное средство, позволяющее пользователям применять свои собственные реализации для этих функций C. Вот пример, который создает несколько различных реализаций для Test:

Выход:

Закрытие

Я надеюсь, что это было информативное чтение о написании привязок к C-коду в Crystal и о том, как мы можем сделать применение этих C-библиотек безопасным с помощью компилятора Crystal.

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

Если вы хотите связаться, я использую @ z64 в Gitter / IRC, а также на форумах сообщества Crystal.

Не стесняйтесь также проверять мои проекты на GitHub:

Https://github.com/z64

Спасибо за чтение! ❤