Как получить строку из функции на Си?

Почему-то эту тему совсем не осветили в документации Crystal, упомянув только то, что передавать и получать строки (при взаимодействии с кодом на Си) можно только как указатели на UInt8. Ни одного примера на эту тему не нашёл, поэтому на всякий случай «восполню пробел»:

lib LibC
  fun ctermid : UInt8*
end
puts String.new(LibC.ctermid)

Функция ctermid возвращает указатель на строку, которая *char с точки зрения языка Си, и она же UInt8* (т.е. Pointer(UInt8)) в языке Crystal. После выполнения функции ctermid в jбласть памяти, адресуемую полученным из функции указателем UInt8*, будет записана последовательность байт, заканчивающаяся байтом со значением 0, что является обычным для Си способом представления строк. И хотя без проблем можно было бы получить результирующую строку, попросту пройдясь в цикле до того самого «нулевого» байта, даже от этой необходимости нас избавил рантайм языка: ведь в Crystal можно создать строку напрямую из UInt8*, что, собственно, и показано в примере выше!

Вот такой простой пример. Для домашней же проработки оставляю вам статью, в которой описаны куда более интересные трюки, допустимые при работе с внешними функциями на языке Си.

За сим до свидания — и до встречи в русскоязычной телеграмм-группе, посвящённой языку Crystal:
telegram.me/crystal_ru

hex_dump строки

Время не стоит на месте, и в Crystal версии 0.26 уже есть прекрасный встроенный метод hexbytes для декодирования строки, содержащей шестнадцатеричный дамп, — в последовательность байт.

А как сделать прямое преобразование? То есть если у нас есть строка — и нужно получить её шестнадцатеричный дамп, какой метод можно использовать?

Ответ с одной стороны пока неутешителен: нет такого метода. С другой стороны, ответ слишком сложен, потому что на архитектуре Intel делать такое преобразование быстрее всего MMX-инструкциями, а значит — использовать встроенный asm-препроцессор (что, разумеется, не будет сделано разработчиками Crystal, поскольку они поддерживают «много разных архитектур» и стараются до последнего всё-таки обходиться собственными средствами языка).

Ну а с третьей стороны — как раз собственными средствами языка всё делается достаточно просто:

def hex_dump (s : String)
  s.to_slice.each_with_object(IO::Memory.new) {|b,s| s << b.to_s(16) }.to_s
end

Здесь мы сначала говорим: «с тем, что было строкой, далее работай как с указателем на побайтовый вектор» (s.to_slice), затем для каждого из байт, на которые указывает наш «слайс», вставляем их шестнадцатеричное представление в объект типа IO::Memory, созданный на лету в конструкции each_with_object. Само преобразование из «байта» в шестнадцатеричное строковое представление ('1' => 31) осуществляется функцией с наименее очевидной в данном случае семантикой: b.to_s(16).

Ну и в конце полученный объект типа IO::Memory преобразуется в строку (.to_s), после чего эта строка и становится выходным значением hex_dump'а

Кстати, это не самый эффективный способ преобразования: hex_dump(io) был бы и проще, и быстрее в случаях наподобие puts hex_dump(s). Но эту задачку я оставлю пытливым умам на домашнюю проработку :)

Рецепт: Передача переменных простых типов по ссылке

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

Если вы сделаете вот так:

def inc(cnt : Int32)
  cnt += 1
end
n = 0
inc(n)
puts n
# "0" will be printed

— ничего у вас не выйдет, поскольку в действительности значение cnt передано не по ссылке, а по значению. Если вдаваться в технические подробности, то значение переменной cnt будет скопировано на вершину стека вызова функции-«метода», а внутри этой функции будет модифицировано именно значение в стеке, а не исходная переменная cnt.

Как же правильно передать значение по ссылке?
Многословность спасёт мир:

def inc(cnt : Pointer(Int32))
  cnt.value += 1
end
n = 0
inc(pointerof(n))
puts n
# output: "1"

(Посмотреть, как работает код...)

Собственно, не сильно отличается от вызова аналогичной функции в Си, только «буков» местами больше, но зато и читается легче.

Всего наилучшего и до новых встреч!

Макросы: Условная компиляция

Для чего нужна условная компиляция и что это вообще такое?

Условная компиляция — это механизм формирования/генерации различного кода в зависимости от внешних условий, заданных на период начала компиляции.

Для чего это нужно? Ну, например, вы сможете генерировать из одного и того же исходного кода разные программы в зависимости от дня недели или наблюдаемой частоты вспышек на Солнце. Шучу конечно, но суть в общем-то отражает верно: это нужно для того, чтобы вы могли собрать программу, адаптированную к тем или иным условиям её (многократного) запуска. Чаще всего в данном контексте вспоминают рихтовку программного кода под целевую платформу: подключаются одни библиотеки, не подключаются другие, в зависимости от разрядности целевой платформы используются одни типы данных и не используются другие (например, на моём древнем нетбуке было бы актуально использовать Int32 вместо Int64). Тем не менее есть и другие, не менее значимые области применения: например, вы можете собрать версию программы с урезанным функционалом, но потребляющую существенно меньше памяти и/или более быструю, вы можете собрать код, из которого будут выброшены за полной бесполезностью условные конструкции, обречённые проверять незыблемые вещи. Таким образом, выудив пару-тройку назойливых if'ов из цикла с большим количеством итераций, вы малой кровью добьётесь весьма ощутимого прироста производительности: ведь проверка условий и условные переходы легко сбивают с толку конвейерный механизм работы современного микропроцессора (префетчинг/предвыборку команд в конвейер) и как минимум загружают его блок предсказания ветвлений ненужной работой.

Ну и конечно условная компиляция помогает быть сухим и чистым, т.е. «не повторять себя» (см. принцип d.r.y) и писать один макрос, ветвящийся в зависимости от значений того или иного параметра — там, где в противном случае пришлось бы писать несколько однотипных макросов или, что ещё «оригинальнее», генерировать макросами другие макросы.

В языке Crystal вы можете даже, пользуясь проверкой flag?(:release), вырезать из финального кода всё, что в противном случае прямо в рантайме проверяло бы, нужно ли выводить какие-нибудь низкоуровневые отладочные сообщения или же делать это и вовсе не следует. «Классический» подход к логированию заключается в том, чтобы устанавливать для приложения уровень отладки в конфигурационном файле или же задавать его параметрами командной строки. Но что если в этом нет никакой объективной необходимости? Что если ваш заказчик не хочет видеть отладку, а вы не хотите тестировать и отлаживать приложение прямо на «живом» заказчике? И если никогда не меняющееся условие «текущий_уровень_отладки >= debug» можно проверить один раз — при компиляции, — то почему бы так и не поступить? И машинного кода в релизе будет поментше, и работать он будет быстрее… Профит? Ну вот то-то же :)

Как же выглядит разрекламированная мной возможность языка Crystal впечатывать один код и не впечатывать другой, называемая также условной компиляцией?

Памятуя об упомянутой выше адаптации под архитектуру, вот вам классический solution для классического же use case, а именно генерация типа-псевдонима для того типа целого, который наиболее актуален для целевой архитектуры:


{% begin %}
    {% long_bl=64; gen_bl=32 %}
    {% if flag?(:x86_64) %}
        {% long_bl=long_bl*2; gen_bl=gen_bl*2 %}
    {% else %}
    # do nothing here
    {% end %} 
    alias Integer = Int{{gen_bl}} | UInt{{gen_bl}}
    alias LongInt = Int{{long_bl}} | UInt{{long_bl}}
    x: Integer=11_u{{gen_bl}}
    y: LongInt=12_i{{long_bl}}
    {% for var in %w(x y) %}
        puts "run-time class of var {{var.id}} is #{{{var.id}}.class}"
    {% end %}
{% end %}


Помимо некоторых интересных особенностей весьма умного движка макросов в языке Crystal, в данном примере на всякий случай показаны и некоторые его (языка) весьма досадные «родовые пятна»: например, для для того, чтобы поместить целое число в переменную целого же типа (не совпадающего с «умолчательным», зависящим от того, под какую архитектуру собран сам Crystal) — компилятору почему-то необходима вот такая откровенно уродливая конструкция, использующая некие самопальные суффиксы, изобретённые разработчиками из Manas, главного спонсора создания Crystal, не иначе как в горячем латинском ночном бреду:

x : Int128 = 11_i128

Видите вот это отвратительное "_i128"? Так вот, скоро вы его не увидите, ну или, вполне возможно даже, что на момент прочтения вами настоящей заметки — уже и не увидите, если только не поставите себе устаревшую версию Crystal. Дело в том, что Crystal 0.24.2, которым я пользуюсь сейчас — это один из последних выпусков, в которых упомянутая выше суффиксная кривизна всё ещё присутствует. Разработчики в курсе проблемы — и находятся на пути к скорейшему её решению.

А за сим про условную компиляцию пока что всё, в следующий же раз я расскажу вам о том, какие классы переменных наиболее общеупотребительны при написании макросов и какие у этих классов и у порождённых по их «образу и подобию» объектов (англ. «инстансов») — есть интересные методы и свойства.

Не переключайтесь и до новых встреч в прямом эфире!

Inline assembler в Crystal

До меня доходили слухи, о том, что якобы в Crystal нет inline ассемблера.
Однако же это не так.


def add(a, b)
    asm("
        movl ($0), %eax
        addl ($1), %eax
        movl %eax, ($0)
    " :: "r"(pointerof(a)),"r"(pointerof(b)))
    a
end

x=12_i32
y=14_i32

puts add(x,y)


Есть только одна проблема: в AT&T ассемблере символ "$" вообще-то используется в качестве отличительного_префикса/сигила для констант. А здесь, как нетрудно заметить, $N — это ни что иное, как позиционный аргумент в блоке asm. И да, не спрашивайте меня, что ещё «r»(a) и странное "::" — сам теряюсь в догадках, но очевидно, что :: отделяет аргументы от кода в блоке asm, а «r»(something) собственно отвечает за определение позиционных $N, каждый из которых будет равен своему something. Сакральное знание о том, как использовать блоки asm, почёрпнуто мной отсюда: github.com/crystal-lang/crystal/blob/e2a1389e8165fb097c785524337d7bb7b550a086/src/fiber.cr#L10

Как считать данные из объекта IO в структуру данных?

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

Самой большой проблемой является то, что данные из IO-объектов могут удобным образом считываться только в типы данных, представленные классам, корректно реализующими некий from_io метод. Такого рода заморочки для решения элементарной и очевидной задачи мне показались некоторым перегибом, а поскольку сопутствующего головокружения от успехов у меня не наступало, я решил поискать другой способ.

Вот, например: можно считать данные из объекта IO (в моём случае — сокета) в некий инстанс типа Bytes. Изучаем API языка — и узнаём о том, что Bytes — это ни что иное как Slice(UInt8). Ну легче конечно не стало, но продолжаем бороздить космические глубины — и натыкаемся на то, что Slice — это вообще-то разновидность указателя Pointer, только с жёстко привязанной к нему логикой проверки границ. Узнав о таких перипетиях бытия, я решил было, что смогу сделать Slice, указывающий на экземпляр, он же инстанс, моей гипотетической структуры данных, а потом просто записать в него данные их сокета. Но конечно же не тут-то было: считать-то можно, но только в Slice(UInt8), если это Slice(C::SomeStruct), например, то Crystal посетует на отсутствие подходящего метода для IO — и откажется компилировать программу. Но лиха беда начало, добрался я и до «родительского» по отношению к Slice классу Pointer. А тот, как назло, тоже строго привязан к типу (при создании нужно указывать тип данных) и порождает, соответственно, Slice(T), где T — это тот же тип, что и в порождающем Pointer'е. Думал я думал — и придумал, что раз я не могу просто создать Pointer на экземпляр структуры данных, куда следует записать данные из IO, то я ведь могу создать 2 указателя: один на экземпляр структуры, а другой, вспомогательный, на UInt8. И это оказалось верным решением, потому что Pointer, к счастью огромному, при создании не требует сразу указывать нечто определённое с присущим этому определённому типом T. Оказывается, указатель даже на Crystal можно создать просто под адресу, т.е. указав некое смещение в памяти. Ну а что же в таком случае мешает получить какое угодно количество указателей, формально относящихся к произвольным типам данных и указывающих, например, на одно и то же место в памяти? Да ничто не мешает!

Осознав всё сказанное выше, я реализовал запись из сокета в структуру данных буквально вот так, просто и элегатно:


lib C
   struct ZbxSenderHdr
     z_sign : StaticArray(UInt8, 4)
     z_stop_byte : UInt8
     z_payload_l : UInt8
   end
end
zhdr=C::ZbxSenderHdr.new
sock.read(Slice.new(Pointer(UInt8).new(pointerof(zhdr).address), sizeof(C::ZbxSenderHdr)))


Т.е. я создал Slice из Pointer(Uint8), который в действительности указывает туда же, куда и pointerof(zhdr) — и записал по адресу Slice'а данные, считанные из сокета.

Не знаю уж, насколько всё это может быть наглядным, но надеюсь, что кому-то кроме меня может оказаться полезным :)

Засим позвольте откланяться — и до новых встреч!

Событийно-ориентированное программирование: простой пример

Даже очень простой и очень неполный: хотим мы, например, получить пользовательский ввод, как-то его обработать в callback'е и получить после того, как всё-таки пользователь соизволит что-то ввести — некий результат на базе введённых данных.

Хотя в примере это не показано (но будет показано в дальнейших примерах, это уж будьте спокойны на сей счёт), но в то самое время, пока пользователь что-то неспеша тупит и вводит — мы можем преспокойно выполнять ещё тысячи зелёных потоков, которые сделают что-нибудь разумное, доброе и вечное. Например, разогреют мышку пользователя до 150 градусов по Цельсию или нервно помигают светодиодами игровой клавиатуры Why not? :)

Итак, вот вам piece-of-code:
class AnyEvent
    def initialize
    end
    def input(&block : String -> Int32)
        promise=Channel(Int32).new
        spawn do
            s=""
            while s.empty?
                print "Press any keys, then hit Enter -> "
                s=gets || ""
            end
            promise.send(block.call(s))
        end
        promise
    end
end

wait_event=AnyEvent.new
input_result=wait_event.input do |keys_was_pressed|
    puts "String is #{keys_was_pressed}"
    keys_was_pressed.size
end
puts "I've received #{input_result.receive} characters as an answer. Thank you SO much! :)"


Обдумывайте.

И счастливых выходных!

Рецепт: как подождать завершения всех порождённых потоков исполнения

И очень просто: нужно в основном (порождающем) потоке подождать получения данных на «вещательном» канале столько же раз, сколько «вещающих» в этот канал потоков мы породили.
Это чем-то похоже на телефонную конференцию: вы ждёте, пока все остальные участники попрощаются (при этом можете не вдаваться в подробности того, кто именно отсалютовал) — и вешаете трубку.

Собственно код:

begin
	ch=Channel(Int32).new
	fibCount=0
	i : Int32
	10.times{|i|
		fibCount+=1
		spawn do
			r=Random.new
			nsec=Random.rand*5.0
			puts "№#{i} launched, will sleep #{nsec}"
			sleep nsec
			puts "№#{i} sleeped #{nsec}, will send its ID to channel"
			ch.send(i)
			puts "№#{i} sended its name to channel, now finishing..."
		end
	}
	
	fibCount.times{
		i=ch.receive
		puts "main: received message from fiber №#{i}"
	}
	
	puts "main: sleeping 1 second"
	sleep 1
	puts "main: Congratulations! Thats DONE!"
rescue ex
	puts "EXCEPTION: #{ex.message}"
end