Казалось бы, какое отношение «хипстерские скрипты для веб» могут иметь к жестким реалиям embedded‑разработки и встариваемых систем, со всей их низкоуровневой работой и ограниченными ресурсами? Увы, но реальность в очередной раз оказалась куда интересней предубеждений, так и появилась на свет эта статья.

Картинка для привлечения внимания, была выложена на ЛОР.
Картинка для привлечения внимания, была выложена на ЛОР.

Что это и зачем

Начну как обычно с цитаты:

mruby is the lightweight implementation of the Ruby language complying with part of the ISO standard. mruby can be linked and embedded within your application.

Словом, это такая особенная реализация языка Ruby, с упором на встраивание и встраиваемые системы — да да, тот самый «кровавый embedded», где царствует чистый C, ссылочная арифметика, malloc() и прочие кошмары и ужасы для современного разработчика.

И вдруг в этом царстве Аида появляетесь вы весь в белом и пишете что-то такое, высокоуровневое:

extend Yeah::DSL                                     
set port: 3000                                        
    
get '/hi/{name}' do |name|    
  "Привет #{name}"    
end
    
ENV['SHELF_ENV'] = 'production'

puts "Запуск.."   
  __main__ [0]

И.. оно просто работает:

Запуск собранного бинарника. Обратите внимание на текст на русском.
Запуск собранного бинарника. Обратите внимание на текст на русском.

И даже вот так:

Ответ сервера в браузере. Обратите внимание на текст на русском.
Ответ сервера в браузере. Обратите внимание на текст на русском.

Круто?

Сколько там пудов соли нужно скушать, чтобы так просто работать с юникодом из Си, тем более в embedded среде?

Кстати вся эта «радость хипстера» еще и собирается в очень небольшой бинарник:

2 Мегабайта на все про все
2 Мегабайта на все про все

Внутри будет «все и сразу»:

MRuby, все используемые библиотеки и само приложение.

Хотя на ЛОРе заметили, что «640кб хватит на всех» это как‑то многовато будет, все же напомню что мы живем в мире копеечных 128Гб флешек и битва за каждый байт свободного места уже не так актуальна как 10 лет назад.

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

Поддерживаемые платформы

К сожалению не удалось найти одним списком все поддерживаемые MRuby платформы, поэтому ограничусь только конкретными найденными примерами.

Из самого интересного:

разработка для Sega Dreamcast и Nintendo Wii, для POS-терминалов, встраивание в iOS приложения.

Ну и собственно встраиваемые системы:

  1. ESP32 , шаблон проекта находится вот тут.

Внешний вид платы ESP32.
Внешний вид платы ESP32.
  1. RP2040 (Raspberry Pi), пример проекта находится тут.

  2. PIC32, пример вот тут.

Тестовый проект

Поскольку «малинки» в очередной раз под рукой не оказалось, было решено реализовать тестовый проект на банальном x86 — была собрана вся цепочка разработки, включая фреймворки, был реализован «Hello world» в виде веб‑приложения, работающего на встроенном веб‑сервере.

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

Для тестового проекта использовалось вот это «чудо»:

Yeah! is a DSL for quickly creating shelf applications in mruby with minimal effort

Если кратко, то это своеобразная попытка реализовать «мини‑Rails, работающий на мини‑Ruby». Вполне себе успешная, надо отметить.

А теперь самое важное:

Фреймворки для mruby представляют собой надстройку, которая в процессе собирает сам mruby и добавляет себя в собираемые бинарники.

Звучит сложно и выглядит страшно, но для embedded-среды является привычным делом.

Так что нам будет нужно получить бинарники mruby и mrbc с упакованным внутрь фреймворком yeah и всеми библиотеками, а затем использовать этот билд для сборки уже своего приложения.

Для сборки фреймворка Yeah! требуется внешний «большой» Ruby и rake, будут работать как 2.x так и 3.x версии.

Клонируем проект с фреймворком Yeah!:

git clone https://github.com/katzer/mruby-yeah.git

Поскольку по‑умолчанию собирается только компилятор mirbc, без интерактивной консоли (mirb) и интерпретатора (mruby), чего не хватит для нормальной разработки конечного приложения, добавляем в файл build_config.rb:

  conf.gem :core => 'mruby-bin-mruby'
  conf.gem :core => 'mruby-bin-mirb'
  conf.gem :core => 'mruby-bin-mrbc'

И запускаем сборку:

rake compile

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

Первая — про заголовочный файл mingw.h:

In file included from /opt/work/tmp/mruby-yeah/mruby/build/repos/host/mruby-r3/r3/src/memory.c:34:
/opt/work/tmp/mruby-yeah/mruby/build/repos/host/mruby-r3/r3/src/mman.h:15:10: fatal error: _mingw.h: No such file or directory
   15 | #include <_mingw.h>
      |          ^~~~~~~~~~
compilation terminated.
rake aborted!

В файле mman.h есть вот такая строка:

/* All the headers include this file. */
#ifndef _MSC_VER
#include <_mingw.h>
#endif

Переменная _MSC_VER не задается при сборке на FreeBSD, поэтому срабатывает вариант по умолчанию — для Windows и MinGW. В качестве исправления, я просто закомментировал этот блок, не заморачиваясь дальнейшими изысканиями.

Вторая ошибка также достаточно банальна и происходит из-за разницы в реализации функции mmap:

/opt/work/tmp/mruby-yeah/mruby/build/repos/host/mruby-r3/r3/src/mman.h:52:9: error: conflicting types for 'mmap'; have 'void *(void *, size_t,  int,  int,  int,  OffsetType)' {aka 'void *(void *, long unsigned int,  int,  int,  int,  unsigned int)'}
   52 | void*   mmap(void *addr, size_t len, int prot, int flags, int fildes, OffsetType off);
      |         ^~~~
In file included from /opt/work/tmp/mruby-yeah/mruby/build/repos/host/mruby-r3/r3/src/memory.c:27:
/usr/include/stdio.h:444:10: note: previous declaration of 'mmap' with type 'void *(void *, size_t,  int,  int,  int,  __off_t)' {aka 'void *(void *, long unsigned int,  int,  int,  int,  long int)'}
  444 | void    *mmap(void *, size_t, int, int, int, __off_t);
      |          ^~~~
rake aborted!

В этом же файле mman.h заменяем:

void*   mmap(void *addr, size_t len, int prot, int flags, int fildes, OffsetType off);

на:

void*   mmap(void *addr, size_t len, int prot, int flags, int fildes, __off_t); 

И повторно запускаем сборку.

Если сборка прошла успешно то в папке build/host/bin будут готовые бинарники:

ls ./mruby/build/host/bin/
mirb	mrbc	mruby

Теперь с их помощью запускаем сборку уже нашего тестового приложения:

/opt/work/mruby-yeah/mruby/build/host/bin/mrbc -Btest_symbol ~/test.rb 

Это сгенерирует файл test.c с вот таким контентом:

#include <stdint.h>
#ifdef __cplusplus
extern
#endif
const uint8_t test_symbol[] = {
0x52,0x49,0x54,0x45,0x30,0x33,0x30,0x30,0x00,0x00,0x01,0x3e,0x4d,0x41,0x54,0x5a,
0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x01,0x0c,0x30,0x33,0x30,0x30,
0x00,0x00,0x00,0xcd,0x00,0x01,0x00,0x05,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x3d,
0x1d,0x02,0x01,0x1f,0x02,0x00,0x2d,0x01,0x02,0x01,0x10,0x02,0x03,0x0e,0x03,0x0b,
0xb8,0x2d,0x01,0x04,0x10,0x51,0x02,0x00,0x57,0x03,0x00,0x2e,0x01,0x05,0x01,0x1d,
0x01,0x06,0x51,0x02,0x01,0x51,0x03,0x02,0x24,0x01,0x51,0x02,0x03,0x2d,0x01,0x07,
0x01,0x06,0x02,0x47,0x02,0x01,0x2d,0x01,0x08,0x01,0x38,0x01,0x69,0x00,0x04,0x00,
0x00,0x0a,0x2f,0x68,0x69,0x2f,0x7b,0x6e,0x61,0x6d,0x65,0x7d,0x00,0x00,0x00,0x09,
0x53,0x48,0x45,0x4c,0x46,0x5f,0x45,0x4e,0x56,0x00,0x00,0x00,0x0a,0x70,0x72,0x6f,
0x64,0x75,0x63,0x74,0x69,0x6f,0x6e,0x00,0x00,0x00,0x0e,0xd0,0x97,0xd0,0xb0,0xd0,
0xbf,0xd1,0x83,0xd1,0x81,0xd0,0xba,0x2e,0x2e,0x00,0x00,0x09,0x00,0x03,0x44,0x53,
0x4c,0x00,0x00,0x04,0x59,0x65,0x61,0x68,0x00,0x00,0x06,0x65,0x78,0x74,0x65,0x6e,
0x64,0x00,0x00,0x04,0x70,0x6f,0x72,0x74,0x00,0x00,0x03,0x73,0x65,0x74,0x00,0x00,
0x03,0x67,0x65,0x74,0x00,0x00,0x03,0x45,0x4e,0x56,0x00,0x00,0x04,0x70,0x75,0x74,
0x73,0x00,0x00,0x08,0x5f,0x5f,0x6d,0x61,0x69,0x6e,0x5f,0x5f,0x00,0x00,0x00,0x00,
0x33,0x00,0x03,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0e,0x34,0x04,0x00,
0x00,0x51,0x03,0x00,0x01,0x04,0x01,0x52,0x03,0x38,0x03,0x00,0x01,0x00,0x00,0x0d,
0xd0,0x9f,0xd1,0x80,0xd0,0xb8,0xd0,0xb2,0xd0,0xb5,0xd1,0x82,0x20,0x00,0x00,0x00,
0x4c,0x56,0x41,0x52,0x00,0x00,0x00,0x16,0x00,0x00,0x00,0x01,0x00,0x04,0x6e,0x61,
0x6d,0x65,0x00,0x00,0xff,0xff,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
};

Это ни что иное как готовый байткод mruby, записанный в виде статичного массива байт.

Параметр -Btest_symbol как нетрудно догадаться отвечает за название этого массива.

А вот так выглядит само приложение для запуска (файл test_stub.c):

#include <mruby.h>
#include <mruby/irep.h>
#include <test.c>

int
main(void)
{
  mrb_state *mrb = mrb_open();
  if (!mrb) { /* handle error */ }
  mrb_load_irep(mrb, test_symbol);
  mrb_close(mrb);
  return 0;
}

Все что тут происходит это просто выдача байткода интерпретатору mruby при запуске обертки. Обратите внимание на строчку:

#include <test.c>

Это и есть включение файла с байткодом mruby.

Ну и наконец сборка конечного тестового приложения:

gcc -std=c99 -static -Os -s -I/opt/work/mruby-yeah/mruby/include -I. test_stub.c -o test_program /opt/work/mruby-yeah/mruby/build/host/lib/libmruby.a -lm -lpthread

Если все пройдет успешно, в текущей папке появится финальный бинарник test_program, который я запускал в самом начале.

Эпилог

Эта статья началась с поста на ЛОРе про «бинарь на руби» с смешным размером меньше мегабайта и кросс‑компиляцией под встраиваемые системы. Я впечатлился и начал копать, так и родилась эта статья.

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

Также как и правительству Японии, которая этот проект финансирует.

Просто потому что убирает целый класс проблем, связанных с разработкой прикладных систем на чистом Си — управление памятью, юникод, строки и так далее.

С нетерпением жду отзывов о реальном использовании.

P.S.

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

0x08 Software

Мы небольшая команда ветеранов ИТ‑индустрии, создаем и дорабатываем самое разнообразное программное обеспечение, наш софт автоматизирует бизнес‑процессы на трех континентах, в самых разных отраслях и условиях.

Оживляем давно умершеечиним никогда не работавшее и создаем невозможное — затем рассказываем об этом в своих статьях.

Комментарии (9)


  1. Areso
    30.09.2024 08:22
    +1

    Очень странное.
    > что мы живем в мире копеечных 128Гб флешек
    флешки на 128 гигабайт живут отдельно от мира встраиваемых систем

    Без малого 2 мегабайта на бинарник веб-сервера (кстати, рекомендую добавлять -h к -la ключам) это примерно столько же, сколько идет (далеко не с каждым) RPi Pico.
    Поэтому байты экономить может быть и нет смысла, все-таки не 80-ые и 90-ые на дворе, но разбрасываться мегабайтами как-то тоже не комильфо.


    1. alex0x08 Автор
      30.09.2024 08:22

      флешки на 128 гигабайт живут отдельно от мира встраиваемых систем

      Ну даже не знаю, из соседней статьи:

      Стартовая модель включает 8 ГБ оперативной памяти и 64 ГБ внутренней памяти.

      Так что думаю все несколько проще стало.

      на бинарник веб-сервера  

      Дело в том что это не просто веб-сервер, а целый фреймворк: REST, JSON, авторизация и так далее и тому подобное.

      Но главное тут в другом: встраиваемый MRuby дает отделение прикладной логики от системной части, т.е. можно реализовать отдачу html и обработку параметров на скриптах, которые при ошибке не уронят все приложение целиком, а системную часть оставить на Си.


      1. Areso
        30.09.2024 08:22

        Да, но в тексте статьи вы приводили не одноплатники (которые, кстати, с каким температурным режимом и допустимостью вибраций работают?), а микроконтроллеры.
        И вот у них памяти ровно на хэллоу ворлд на веб-сервере или даже меньше.

        > встраиваемый MRuby дает отделение прикладной логики от системной части, т.е. можно реализовать отдачу html и обработку параметров на скриптах, которые при ошибке не уронят все приложение целиком, а системную часть оставить на Си.
        считается, да.


        1. alex0x08 Автор
          30.09.2024 08:22

          , а микроконтроллеры.

          Вообще речь про embedded разработку, что на сегодняшний день либо arduno/pi либо промавтоматика со своими законами и АСУ.

          По крайней мере так обстоят дела в тех проектах к которым я имел или имею отношение - если даже в самокаты сейчас arduno ставят, чтож теперь сделаешь.


          1. Areso
            30.09.2024 08:22

            Да, но в Arduino у вас нет гигабайтов, а есть килобайты
            https://store.arduino.cc/en-cy/products/arduino-mega-2560-rev3

            Flash Memory - 256 KBSRAM - 8 KBEEPROM - 4 KB
            Flash Memory - 256 KB
            SRAM - 8 KB
            EEPROM - 4 KB

            Неправильно смешивать МКшки (промышленные или нет) и Single Board PC (промышленные или нет).


            1. alex0x08 Автор
              30.09.2024 08:22

              Неправильно смешивать

              По идее да:

              Generally Ethernet is not built into microcontrollers. First you need a jack which can convert Ethernet signals into signals read by a microcontroller (this is generally called 'magnetics'). Then you need a TCP/IP stack, and then on top of that you need DHCP, DNS and whatever other protocols you want to use. So the actual microcontroller you use doesn't matter a whole lot. If you get something very powerful like an ARM with Linux running on it, then developing for it would be very simple, almost the same as writing a network application running on a desktop PC running Linux. Or you could go with something less powerful & cheaper like an AVR or PIC.

              Но там же чуть ниже:

              Lots of the TI Luminary microcontrollers (ARM Cortex-M3) have an onboard ethernet MAC. It needs an external crystal and ethernet PHY (connector + magnetics).

              И как быть?


              1. Areso
                30.09.2024 08:22
                +1

                да сеть может быть даже на более "ущербных" МКшках, я вам не про сеть, я вам про вопрос куда вы двухмегабайтных бинарь на него класть собираетесь.

                Вот, к примеру, чуть более жирный вариант, когда есть и сеть на борту:
                https://www.espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf

                Flash 4 MB - optional on-chipSupports up to 16 MB  external memory
                Flash 4 MB - optional on-chip
                Supports up to 16 MB external memory


    1. Keroro
      30.09.2024 08:22

      Ну PIC32MX из примера №3 вполне себе встраиваемая система (там 256КБ ROM и 64КБ RAM). Правда, там вроде бы некая 16 бит разновидность используется на 40КБ.


      1. Keroro
        30.09.2024 08:22

        Но в качестве небольшого оффтопа, работать на микроконтроллере с UTF не такая уж большая проблема. Я использую этот сниппет на 45 строк с форума LOR, и он прекрасно работает, по крайней мере с кириллицей.