
Комментарии 54
Классно, нужно будет попробовать)
ну тоесть получается на десктопе вся та портянка зависимости получается ляжет в память по счетчику ссылок или только родители/дети, я не знаю как в ембединге вежет себя ява и не до конца понимаю внутрянку языка, но там помойму внутрянка решающая, тоесть это возможно придётся переписать весь язык, ведь нью может запускать каскад нью под капотных типо нетривиальных, они тоже подцепляются на счетчики интересно, если так, то на интрузивном счетчике ссылок на С++ можно целый аналог явы чтоли сварганить ?) мне кажется это было бы проще, хотя то как ява оптимизирована в плане запуска байт-кодов восхищает конеш........ интересно очень, но ничо не понятно )))
Вопрос про подсчёт ссылок: кольцо из ссылающихся объектов приведёт к утечке? Сборщик мусора двигает объекты в памяти при сборке или нет? Если нет - как решается проблема фрагментации? Есть ли escape analysis, когда объект создаётся внутри функции, гарантированно не утекает и тут же выкидывается? (Тогда можно вообще счётчик ссылок не заводить)
И ещё одно замечание про оверхед со ссылками и прочее - в С# в системе типов изначально были ещё и структуры (а в java их всё никак не приделают), и кажется что для МК они нужны. Идея структуры в том, что она передаётся по значению (прям как си), и позволяет создавать меньше объектов и плотнее хранить данные в памяти.
P.s. я сам пробовал задизайнить язык, но я правда пришёл к ещё более радикальной идее - у меня один и тот же класс может передаваться и по ссылке и по значению. У меня у ссылок на объект в куче тип Gc[Smth], а у объекта по-значению - Smth, и можно один и тот же класс точечно и на куче создавать и на стеке (например, если я как программист знаю, что он временный).
P.p.s. какие-то мысли про язык и процесс разработки (сам язык сильно отличается, но надеюсь какие-то вещи могут пригодиться) : https://kright.me/2026/07/06/delaiu-svoi-iazyk-programmirovaniia/
1. Кольцевые ссылки: Да, в текущей реализации это приведет к утечке. На данном этапе я сознательно не усложнял архитектуру (нет ни weak-ссылок, ни trace-компонента), так как в рамках 8-битных систем сложные цикличные графы объектов практически не встречаются — логика приложений пишется максимально плоско. Избегание циклов сейчас — это ответственность прикладного программиста.
2. Перемещение объектов: Нет, никакого перемещения и полноценного Garbage Collector здесь нет. Механизм максимально детерминированный: чистый Reference Counting с мгновенным освобождением памяти при обнулении счетчика. Поэтому адрес объекта в куче (HEAP) является его постоянным и уникальным идентификатором.
3. Фрагментация: Проблема решается на уровне кастомного менеджера памяти. Аллокатор работает по принципу битовой маски, где 1 бит отвечает за блок в 8 байт (дискретная сетка/слоты). Для 8-битной архитектуры с небольшими объектами такой подход сводит внешнюю фрагментацию к минимуму при крайне низком расходе памяти на метаданные аллокатора.
4. Escape analysis: На данный момент отсутствует и будет рассмотрен как задача оптимизации. И таких задач не мало — Альфа версия. Вы правы: если объект гарантированно не покидает пределы метода, то подсчет ссылок не нужен - это вопрос оптимизации кода.
Важный нюанс, который часто упускают при обсуждении Escape-анализа для 8-битных систем:Статический анализ компилятора никогда не покрывает 100% кейсов. Если алгоритм не может гарантированно доказать, что объект "не убегает", он обязан свалиться в безопасный режим и включить подсчет ссылок в рантайме.
На масштабах прошивок для 8-битных МК, где методы и жизненный цикл объектов экстремально короткие, а память сильно ограничена, доля успешно доказанных и при этом эффективных оптимизаций через Escape-анализ будет ничтожной. Усложнение компилятора получается значительным, а реальный выигрыш в байтах или тактах — копеечным. В условиях жестких физических ограничений внедрение таких тяжелых механизмов оптимизации просто экономически и инженерно нецелесообразно. Особенно когда есть пласт более приоритетных задач.
Кольцевые ссылки: Да, в текущей реализации это приведет к утечке.
Интересно! А вы зачем вообще ссылки считаете, у вас же вот даже в вашем примере:
Timer blink = new Blink();
while(true) {программа никогда не кончается (while(true) !!!)? И это нормальная ситуация для 8-битных контроллеров. Получается вы это как-то не учли что ли? На 8-ми битных процессорах сборщик мусора просто не имеет смысла, по факту, какая нафик утечка, куда? Мусора просто не должно быть! При очень ограниченных ресурсах это непозволительная рокошь.
Извините, я не понимаю что Вы хотели этим сказать?
Я просто оставлю это здесь:

или это из коллекции

Какая связь между незавершающейся программой и подсчётом ссылок? Ну сделал он в примере new до цикла, а в примере посложнее сделал бы в каком-нибудь методе, который вызывается в цикле и возвращает это значение. В C для этого возвращался бы указатель.
Динамическое выделение памяти не является непозволительной роскошью, реально используемой динамической памяти в каждый момент времени обычно сильно меньше, чем если бы память под все используемые данные была бы выделена статически.
И утечки тут не при чём, утечка - это когда память выделяется и не освобождается, в языках с динамической памятью и сборкой мусора нет явного освобождения, и если ссылок не осталось, то утечек не будет.
ну не знаю что для вас является аргументом. Ну вот например:
Если вы освобождаете память динамически, значит вы ее где-то и выделяете динамически, значит у нас какой-то алгоритм должен работать, индексации-распределения блоков этой памяти. Но на скажем 4-килобайтах оперативной памяти и скажем 20 МГц тактовой частоты процессора это выглядит как совсем лишняя работа, которую вообще говоря всегда(!) можно избежать-решить на уровне проектирования программы с помощью какой-то простейшей индексации например, тех же linked list техник...
Тащить в такую систему универсальный сборщик мусора, на все случаи жизни, мне кажется большой перебор. По крайней мере я всегда находил способ решить это примерно так как я описал. Утечки на таких ограниченных системах должны исключаться на этапе проектирования программы, иначе вам там ничто не поможет добиться хотя бы стабильности.
Вы практически описали концепцию J8B, только предложили делать её руками. Ваши кастомные связанные списки (linked list) и ручные индексы в Си - это и есть динамическое распределение ресурсов, на которое процессор потратит ровно те же такты. Разница лишь в том, что в Си Вы пишете это вручную под каждую задачу (регулярно ловя баги с указателями), а в vm5277 этот алгоритм написан один раз, вылизан на ассемблере и скрыт в рантайме.
Но самое забавное - динамика в J8B добровольная. Если Вы так сильно её отвергаете, вас никто не заставляет использовать new. Вы вольны объявлять переменные глобально, делать методы статическими и писать всю программу на чистой статике, как Вы привыкли в Си. Язык дает выбор: хотите - пишите в старом процедурном стиле, хотите - используете мощь безопасного ООП с экономией ОЗУ
Зачем вообще использовать ООП и интерфейсы на микроконтроллерах?
В Си-подходе, если вам нужно перенести код экрана с аппаратного I2C микроконтроллера ATmega328 на программный ногодрыг (Bit-bang) для ATtiny (где аппаратного I2C нет в принципе), вам придется переписать всю библиотеку дисплея, обмазав её макросами #ifdef. А если у вас в системе 5 разных дисплеев и 3 разных датчика - Си-код превратится в нечитаемый ад.
В ООП-модели J8B эта проблема решается через полиморфизм интерфейсов. Вы пишете драйвер дисплея один раз в жизни. Ему всё равно, какая под ним железка. В конструктор экрана вы можете передать объект HardwareI2c на одном чипе, или SoftwareI2c на другом. Код самого экрана не изменится ни на одну строчку.
ООП на 8 битах нужно не ради абстракций ради абстракций. Оно нужно, чтобы избавиться от бесконечного переписывания драйверов, защитить ОЗУ от багов с указателями и собирать прошивки из готовых, безопасных и на 100% переносимых кубиков как в LEGO - все это дико экономит время и нервы разработчика а также снижает его порог вхождения в данную отрасль.
Этот проект родился не на пустом месте. У меня есть собственный масштабный проект умного дома, где десятки прошивок (датчики, шлюзы, исполнительные устройства) написаны мной на чистом ассемблере на базе моей же RTOS (core5277).
Я знаю регистры и ассемблер досконально. И именно на практике я упёрся в стену: когда устройств много, а их бизнес-логика усложняется, нативный кодинг (даже на Си) превращается в ад из-за нулевой переносимости кода и постоянного риска словить скрытый баг с памятью.
vm5277 - это закономерная эволюция моей ассемблерной RTOS. Если вам нужно один раз написать простую мигалку — пишите на Си, моё решение вам не нужно. Но если вы строите распределенную экосистему устройств, где бизнес-логика постоянно растет, а чипы из-за дефицита или рынка приходится менять, - безопасность, читаемость и кроссплатформенность J8B окупают себя на 100%
Если вам нужно один раз написать простую мигалку — пишите на Си, моё решение вам не нужно.
Когда есть С++ я пишу на С++, но new там идет в огромной ран-тайм библиотеке стандартной и я очень замечательно обхожусь без этой библиотеки и без new , соответственно. Слава богу чтобы вызвать деструктор для объекта на стеке при выходе из функции new не нужен и это очень удобно. Но С++ на мелких процах конечно никто не делает, наверно, но я уже давно не писал на мелких.
Кстати на процах посерьезнее уже не обойтись без библиотек. Например, мы использовали LWIP в исходниках - я там даже что-то тоже поправил на уровне работы с регистрами почти. Вы LWIP тоже на Джаву переведете :) ? Библиотеку c TCP/IP стеком тоже напишете под MAC контроллер переферийный?
"Вы LWIP тоже на Джаву переведете :)" - Уже была такая мысль. Может быть. Не забывайте, что TCP/IP это уже существующий и множество раз реализованный стек. Его не нужно изобретать.
Разработчику способному создать RTOS на ассемблере такая задача вполне по зубам. Особенно если будет команда. Особенно, если проект наберет популярность. Может быть даже мой любимый SCTP реализую - ведь это не про 8-бит, это про vm5277 который я планирую и на десктопы развернуть.
P.S. Вы меня пытаетесь напугать MAC'ом? Серьезно?
Знаете почему я люблю писать на Java и на ассемблере но никак не на Си?
Потому что все эти наработки на Си превратились в тонны спагетти-кода, с кучей дефайнов, с кучей полностью не читабельного кода. Там чтобы понять чужую бизнес логику нужно прочитать талмуды размером с войну и мир, потому что все перемешано и связано, даже комменты не помогают.
И в итоге вместо нескольких минут ты уже сидишь пятый час чтобы понять элементарную бизнес логику.
Реализация сети? Не спорю, если реализовать все до крайности - на это годы на Си уйдут. Но я, на своем опыте прекрасно знаю, что если бы у этих библиотек был бы универсальный HAL - они были бы значительно меньше. А реализовать ethernet, ip, udp - это уровень второго курса универа. TCP да гораздо сложнее - но там и половины функционала в рамках МК не нужно.
Так что да, запилить поддержку пары Ethernet микросхем для МК (для начала только с UDP) задача тривиальная. Я больше сил потрачу на раскуривание сишного кода.
Попробуйте поработать с Java - напишите несколько сетевых утилит, серверов, протоколов. Попробуйте на ней помигать светодиодами на той-же Raspberry. А потом вернитесь к Си с его фишками и оцените как удобно на нем писать портируемый код скажем на avr и stm32
Попробуйте поработать с Java - напишите несколько сетевых утилит, серверов, протоколов.
ну я тут как то LDPC декодер написал на Java для статистических испытаний, потом переписал на C#. C# мне болльше нравится. Вы бы хоть поинтересовались прежде чем предлагать что-то попробовать. Вы вот можете сказать в чем принципиальное отличие Java от C#? Я вот особой принципиальной разницы не заметил если наличие функциональности в библиотеках не сравнивать.
Вот сугубо практическая задача. dma принимает с внешнего источника пакеты - кольцевой буфер, прерывания на середине и конце буфера - ну как обычно. Как это на этой недожаве реализовывать?
В vm5277 работа с прерываниями, DMA и регистрами железа пишется исключительно на нативном ассемблере внутри ядра RTOS, а не на прикладном языке. Оверхед там близок к абсолютному нулю.
Прикладной язык J8B используется только как безопасная ООП-обертка над этой логикой. Ассемблерный обработчик DMA (по прерываниям) точно так же складывает данные в кольцевой буфер в куче, а из J8B вы просто вызываете потоковый read().
Посмотрите, как это уже сделано для обычного аппаратного UART:
https://github.com/w5277c/vm5277/blob/main/rtos/avr/drivers/uart/hw_full.asm
https://github.com/w5277c/vm5277/blob/main/runtime/drivers/ifaces/UartHwFd.j8b
Для DMA можно сделать аналогично. Но программный кольцевой буфер не обязателен — можно писать/читать напрямую в созданные в HEAP массивы на основе new byte[]. А еще есть механизм RtosCallback, через который низкий уровень может вызывать J8B-код. Пример: https://github.com/w5277c/vm5277/blob/main/examples/j8b/timer1/src/Main.j8b
Какое конкретно будет принято решение — зависит от задачи. Когда проект дойдет до поддержки DMA (а это актуально для 32-битных архитектур, а не 8-битных), гибкость архитектуры позволит выбрать максимально эффективное решение. И под конкретную задачу оно, вероятнее всего, окажется чище и оптимальнее, чем на C++.
А самое важное - работа с DMA - это не прикладной уровень. Разработчику нечего там делать - это задача ядра системы.
Я понимаю, что мир Си заставляет разработчика писать код буквально на всех уровнях единой простыней. В этом очередное важное отличие моего решения - прикладной разработчик не изучает даташиты, регистры периферии МК, особенности компилятора. Он просто создает объект нужного класса - например BMP580 и работает с его методами не вникая что там лежит уровнем ниже.
В этом очередное важное отличие моего решения - прикладной разработчик не изучает даташиты, регистры периферии МК, особенности компилятора. Он просто создает объект нужного класса - например BMP580 и работает с его методами не вникая что там лежит уровнем ниже.
Так вы собрались сделать библиотеку классов объектов на все случаи жизни получается? Если не изучать даташиты, регистры периферии МК, особенности компилятора то, видимо, придется изучать описание методов и поведения ваших объектов и все равно компилятора этих объектов в объектный код, только вашего(!) компилятора, что-то очень сомнительно что это может в принципе быть намного проще, а главное что этого будет достаточно для решения любой эмбедед задачи.
Я разделяю функционал:
1 - то что не требует работы на низком уровне (не критично по времени, не требует работы с IO регистрами (кроме GPIO), не требует максимальной оптимизации) - пишется на j8b (в том числе и драйвера) - это дает полную переносимость.
2 - драйвера, которые не привязаны к железу а только к интерфейсам (i2c, uart, spi и прочие) - работают также на j8b - опять-же полная переносимость (может быть также реализовано на программных интерфейсах)
3 - остальной код реализуется на ассемблере и должен быть реализован в ядре (но часть его также может быть на j8b)
4 - рантайм будет и уже содержит некоторые драйвера конечных устройств (типа bmp580) - прикладнику достаточно просто использовать класс датчика подставляя ему нужные классы реализации интерфейсов. Для этого действительно не нужно знать даташиты.
5 - прикладник может также написать сам драйвер устройства если он основан на популярных и поддерживаемых в vm5277 интерфейсах и даже сами интерфейсы если им достаточно GPIO. И здесь ему также не нужно знать даташиты на МК - как реализовать интерфейс (прерывания, IO регистры и прочее)
6 - реализация стандартных интерфейсов и прочей периферии конкретного семейства МК - задача разработчиков нативной части (ядро и RTOS на ассемблере) - но это задача не прикладника.
Да - прикладнику нужно будет знать названия классов устройств и методов. Для этого по большей части достаточно простого описания методов. например:
public SpiSwHd(byte dataPort, byte clkPort, boolean fastMode, byte delay, boolean cpol, boolean cpha, boolean lsbFirst) {...}
public short comm(byte[] sendBuf, short sendLen, byte[] recvBuf, short recvLen, short timeout) throws DriverException {...}
Это далеко не то-же самое что изучение даташита МК на предмет IO регистров работы с SPI
Вот еще пример:
// Регистрируем обработчик (менеджер сам определит оптимальный механизм детектирования) regId = PinEventManager.register(ArduinoUno.PIN_D2, inst, PinEventManager.TRIGGER_MODE_ANY);
Прикладнику даже не нужно особо заморачиваться как именно реализуется обработка смены состояния пина (прерывания INT или PCINT и т.п.). Конечно если он захочет оптимизировать все по максимум - ему придется спуститься на уровень ниже - мое решение это позволяет - вплоть до ассемблера. (PinEventManager - еще в разработке)
Не попытка ли это плодить сущности? Настроить таймер/уарт/etc это лишь записать насколько регистров. Это довольно просто и интуитивно. А плодить rtos, и еще сверху оверхед - разумно ли это? Я не критикую, просто дискуссия.
Только на atmega328 три таймера, где-то WD еще подключают - настраиваются по-разному (элементарно что-то 8 бит, что-то 16). На других avr таймеров другое количество. Где-то UART, где-то USART, где-то USI, где-то вообще ничего. А еще и имена регистров отличаются а также их биты и в принципе их порядок и функционал. И это только Atmega и UART, а еще ATtiny - может сильно отличаться. И это только платформа AVR, а добавьте туда PIC, STM8 и не дай бог STM32.
Сколько у Вас займет времени перенести работу I2C экрана с Atmega328 на attiny где нет I2C? Я даже не буду говорить, что примеры которые я видел на си подобной реализации были с существенными ошибками.
А если Вам нужно 10 таймеров? И пять UART'ов? Будете покупать в несколько раз дороже чип?
Вы просто не представляете о чем Вы говорите - переносимость - это очень серьезная проблема. Попробуйте сделать самое элементарное - прослойку которая будет просто по пину предоставлять возможность вызова процедуры при смене любого выбранного пина как при работе с INT для простого семейства Atmega. Опытный системный разработчик потратит несколько дней на такую задачу. А для Вас я смотрю это просто и интуитивно.
Я даже уже не говорю о том, что работа с регистрами это не безопасно в отличии от вызова метода.
Сколько у Вас займет времени перенести работу I2C экрана с Atmega328 на attiny где нет I2C?
интересно было бы посмотреть на некий универсальный класс-объект для I2C экрана который каким то чудом будет одинаково работать
и на Atmega328
на attiny где нет I2C !!!
Вы просто не представляете о чем Вы говорите - переносимость - это очень серьезная проблема.
но до переносимости еще нужно решить проблему возможности реализации с заданными характеристиками. Обычно нужен не какой-то абстрактный I2C экран, а I2C экран с четко заданными характеристиками - таймингами например, и он не в сферическом вакууме должен работать, а в жестких условиях конкуренции за очень ограниченные ресурсы с другими пользовательскими функциями программы.
Вы что, собираетесь сделать универсальные-божественные классы, которые подходят под любую задачу? Мне кажется, это похоже на задачу алхимии, вы пытаетесь создать лекарство от всех болезней и ключ от всех дверей, не повторяйте ошибок прошлого.
;)
Этот проект начат не с чистого листа. У меня есть проект RTOS для AVR на ассемблере https://github.com/w5277c/core5277
Вот в нем есть программные драйвера, например https://github.com/w5277c/core5277/blob/devel/core/drivers/i2c_su.inc - I2C Slave основанный на USI
В ядре текущего проекта для AVR будет набор программных драйверов. Конечно они будут более медленные, чем аппаратные и будут больше потреблять ресурсов, но они будут.
При этом огромное преимущество дает верхний уровень - можно писать драйвера (не зависящие напрямую от железа) на j8b один раз с полной переносимостью кода. Пример https://github.com/w5277c/vm5277/blob/main/runtime/drivers/ifaces/SpiSwHd.j8b
Вот пример реализации в моем проекте светодиодной матрицы 8x8 на базе MAX7219 https://github.com/w5277c/vm5277/blob/main/runtime/drivers/display/LedMatrixMax7219.j8b в его конструкторе создается программный интерфейс (аналогично будет и для любого экрана с I2C) iface = new SpiSwHd(dinPort, clkPort, true, 0, false, false, false);
А вот кстати и I2C символьный экран HD44780: https://github.com/w5277c/vm5277/blob/main/runtime/drivers/display/HD44780.j8b там вообще заложен layer как прослойка реализующая работу с разными интерфейсами.
Конечно это все черновые наработки (проект в Альфе, и я недавно поломал I2C и часть примеров перестала работать) но все что у меня есть в примерах (а там разные устройства) почти все они работоспособны (а то что сломал я скоро починю). Но тем не менее архитектура вполне рабочая.
Просто не забываем про мощь ООП модели.




Ну и на закуску, пример работы с ws2812b который максимально привередлив к ресурсам и таймингам МК.

P.S. Конечно в реальности есть куча жестких ограничений по ресурсам, которые не дадут к примеру запустить тот-же ws2812B на частоте менее 8МГц (на 8 - это уже предел возможностей). Но здесь мы уже бессильны.
P.P.S. Еще можно сюда заглянуть https://github.com/w5277c/vm5277/blob/main/examples/j8b/max7219/src/Main.j8b - там вообще магия и она работает прямо сейчас.
ну вот! Что и требовалось доказать! По моему гораздо проще изучать даташиты, регистры периферии МК, особенности компилятора, чем то разнообразие надстроек над этим богатством, которое вы (еще только) собираетесь предложить, учитывая что:
Конечно это все черновые наработки (проект в Альфе, и я недавно поломал I2C и часть примеров перестала работать)
Но успехов вам, конечно. Большая-продолжительная работа в конце концов находит практическое применение, хоть и не всегда там на что изначально была цель.
Это целиком Ваше право и Ваше мнение. Спорить с этим никакого смысла не имеет.
"которое вы (еще только) собираетесь предложить" - сейчас я привлекаю разработчиков к Альфе, а не пользователей к релизу. Вы же меня упрекаете что на Альфе не все готово - естественно не все готово - именно об этом я и пишу в статье.
Разумно ли вообще сейчас что-то пилить на древних устаревших и дорогих mcu? Китайские кортексы ххх32 дешевы, мощны и куча периферии.
Конечно разумно.
1 - 8 бит имеют свой сегмент и он очень не маленький и у них есть свои преимущества которых нет у чипов на базе Cortex
2 - 8-бит - это старт проекта - если проект зайдет, то он придет и на 32 бита и на десктопы.
3 - вообще тема использования мощных МК достаточно сомнительная, потому что их можно заменить на SoC с еще большими преимуществами.
Наивный подсчёт ссылок менее эффективен, чем tracing GC (источник - https://www.researchgate.net/publication/225216046_Uniprocessor_Garbage_Collection_Techniques). Если собираетесь останавливаться на подсчёте ссылок, посмотрите на Perceus (https://dl.acm.org/doi/10.1145/3453483.3454032).
Я не претендую на гениальность, и тем более не считаю, что научное сообщество просто так ест свой хлеб.
Но есть два важных момента:
1 - 8-битный МК - это просто, здесь нет многоядерных решений, нет кэша процессора и очень простая архитектура процессора в целом.
Кроме этого, сильно ограниченные ресурсы, где лишняя логика в рантайме - серьёзный оверхед.
2 - наивный подсчёт ссылок без оптимизации со стороны компилятора - это действительно не оптимально, потому что обязательно найдутся условия, в которых умный компилятор сможет доказать определённые кейсы и оптимизировать код, убрав лишние операции работы со ссылками - я это понимаю.
Но такие алгоритмы мной сейчас не рассматриваются, потому что главная цель - наработать функционал.
Оптимизация кода будет позже и будет затрагивать разные аспекты. Возможно, вообще будет только в коммерческой версии.
К тому-же бэкенд кодогенератор и ядро не обязательно должно быть идентичным на всех платформах, более того, они действительно будут сильно отличаться если сравнить 8 бит и 32 бита (например менеджер динамической памяти) - что не повлияет на код верхнего уровня.
Спасибо за информацию. Когда я подойду к вопросу оптимизации (а в проекте явно есть что оптимизировать) - Ваши ссылки мне будут очень полезны.
Добрый день!
Сердечно поздравляю вас с таким замечательным успехом!
Я сам очень интересуюсь разработкой компиляторов и разработкой ОС.
Думаю, что я готов был бы попробовать поддержать ваш проект, но у меня есть несколько пожеланий по поводу его развития.
Очень прошу не составлять о них первого категоричного мнения, а попробовать беспристрастно разобрать плюсы и минусы.
1) Очень хотелось бы иметь маркированное объединение с возможностью сопоставления с образцом. Это позволяет гораздо изящнее строить абстракции. Кроме того, я слышал, что прошивки для микроконтроллера часто представляют из себя конечный автомат. А то, что я предложил (алгебраические типы данных) очень хорошо подходят для этого дела.
Подробнее можно прочитать вот здесь: https://habr.com/ru/articles/1033910/
2) В Java объявить ещё один тип - это достаточно муторное занятие. Нужно создавать отдельный файл, ну и так далее. Но опыт показывает, что полезно создавать не мало больших типов, а много маленьких. А в Java это не удобно.
Я бы хотел, чтобы вы добавили возможность создавать типы в любом лексическом окружении. Это просто сделать, но это очень поможет программистам.
3) Очень хотелось бы, чтобы вы попробовали несколько разных языков: Zig, Lean, Clojure, Prolog если не сделали этого раньше. Это поможет прекрасно расширить кругозор!
4) Не могли бы вы попробовать программу для проверки моделей Alloy model checker? Она позволяет выявлять баги в архитектуре.
Скажите пожалуйста, а почему вы ориентируетесь именно на синтаксис Java, а не на синтаксис Kotlin?
У меня есть ещё много вопросов, но не всё ведь сразу!
Спасибо вам за вашу разработку!
Спасибо за ваши предложения и ссылку на статью! Однако я хотел бы пояснить, почему наши взгляды на проектирование языков программирования принципиально расходятся.
Исходная точка: физические ограничения против теории типов Мой подход исходит из физических ограничений целевого железа (8-битный МК и 2 КБ ОЗУ). Это не абстракция, а жесткая реальность. Каждая фича языка в vm5277 должна платить за свое существование байтами Flash/RAM и тактами процессора. Ваш подход, как я понял, идет от выразительности и корректности на уровне типов. Это прекрасно там, где есть гигабайты ОЗУ и JIT-компилятор, но избыточно для микроконтроллеров. Я выбрал ООП-модель Java в качестве основы, потому что она узнаваема миллионами, достаточна для задач embedded и поддается глубокой оптимизации под 8 бит (наш RTTI упакован во Flash, VMT в ОЗУ отсутствует, а объекты остаются “плоскими”).
Про маркированные объединения и pattern matching: В J8B полиморфизм интерфейсов уже решает задачи подмены реализаций. Внедрение ADT потребует кардинальной перестройки парсера и семантического анализатора, что нецелесообразно. А текущий enum потребляет 0 ресурсов рантайма.
Про конечные автоматы: Вы предлагаете реализовывать их через ADT и match. В моей же модели автомат — это класс, инкапсулирующий состояние. При необходимости он может управляться отдельным легким потоком (Thread), что дает изоляцию и параллельность, либо работать в рамках общего цикла.
Мы смотрим на язык с разных сторон: вы — со стороны теории типов, я — со стороны эффективной кодогенерации под ассемблер.
По поводу локального объявления типов: Объявить новый тип в лексере и парсере, да и в семантике — это как раз легкая задача. А вот с кодогенерацией существенно сложнее — для каждого нового типа нужно с нуля прописать логику сравнений, арифметики, присваивания и приведение типов и их взаимодействия друг с другом на уровне регистров.
По поводу расширения кругозора (Zig, Lean, Clojure и др.): В моем инженерном багаже: C, C++, Java, C#, Bash, Assembler, Basic, Python, Kotlin, Dart и многое другое. Мне не нужно абстрактно расширять кругозор ради самого кругозора. Я решаю конкретную практическую задачу, стремясь реализовать её оптимально, а не универсально.
По поводу Alloy model checker: Формальная верификация моделей потребует от меня колоссального количества сил и времени. Я предпочитаю потратить этот ресурс на написание кода, развитие рантайма и исправление живых багов альфа-версии. Прагматизма в Alloy для хобби-проекта на одного человека я сейчас не вижу.
Почему Java, а не Kotlin? Если отбросить маркетинговое продвижение Kotlin, то в реальном мире первыми по популярности среди прикладных языков идут C и Java.
Именно по этой причине мой язык максимально похож на синтаксис Java. Изменено по большому счету только то, что экономит ресурсы или банально удобнее в разработке и при этом ничего особо не стоит для рантайма.
Буду рад вашим дальнейшим вопросам, но очень прошу учесть: я практик и всегда исхожу от конкретной инженерной задачи. Чистая теория, как и синтаксический сахар, мне не особо интересна.
Исходная точка: физические ограничения против теории типов Мой подход исходит из физических ограничений целевого железа (8-битный МК и 2 КБ ОЗУ). Это не абстракция, а жесткая реальность. Каждая фича языка в vm5277 должна платить за свое существование байтами Flash/RAM и тактами процессора. Ваш подход, как я понял, идет от выразительности и корректности на уровне типов. Это прекрасно там, где есть гигабайты ОЗУ и JIT-компилятор, но избыточно для микроконтроллеров.
У вас ложная дихотомия. Корректность и сложность системы типов ортогональны размеру скомпилированного бинарника. Они могут усложнить компилятор, но не требуют сложности от рантайма. Например, явная nullability в котлине позволяет на этапе компиляции явно доказать, что какие-то ссылки нулевыми не будут и спасает от ошибок. Или, например, лайфтаймы в расте существуют только во время компиляции как доказательство корректности, в скомпилированном коде их нет. (Конкретно в Расте кстати не всё хорошо, авторы игнорировали теорию типов, в итоге в языке оказались проблемы, которые просто так не выкорчевать). И всякие constexpr и consteval фичи в с++ это как раз про то, чтобы помочь компилятору вынести какие-то вычисления на этап компиляции, а не в рантайм.
Дело не в самом усложнении рантайма, а в усложнении библиотеки бэкенд кодогенератора без какой-либо существенной практической пользы (который требуется реализовывать под каждую платформу отдельно) - я не достаточно развернуто ответил на этот вопрос ранее.
Более того, я объяснил существенную причину почему выбран Java синтаксис. Да и в принципе вопрос выбора языка и его ситнаксиса я не рассматриваю, кроме изменений который действительно будут существенно полезны для разработки прошивок на 8 бит Мк, как например замена signed типов на unsigned.
Все перечисленные вами примеры требуют нетривиальной системы зависимостей между типами, подстановки, обобщений и проверок на уровне AST/семантики. Это всё ложится на этап компиляции и кодогенерацию. Для моего компилятора под 8 бит это означает не просто "ещё одна проверка", а перестройку всей инфраструктуры вывода кода которая (изначально достаточно сбалансирована) — и это я считаю неоправданным без явной, измеримой выгоды для конечной прошивки
Конкретно в Расте кстати не всё хорошо, авторы игнорировали теорию типов, в итоге в языке оказались проблемы, которые просто так не выкорчевать
Извините за оффтоп, но можете ли вы порекомендовать источники, в которых сруктурировано изложены упомянутые вами проблемы? Мне для общего кругозора.
Есть такое видео, но его сложновато слушать: https://www.youtube.com/watch?v=1iPWt1gvT_w
И ссылки на почитать. https://habr.com/ru/articles/1033328/ https://github.com/rust-lang/rust/issues/25860 https://github.com/rust-lang/rust/issues/135011
Поискать можно по словам типа “Rust type system unsoundness”
Ещё я не нашёл ссылки, но суть в том, что некоторые стандартные классы в языке можно было бы сделать монадами, но так сложилось, что их написали как набор отдельных самостоятельных классов со своими уникальными названиями методов.
2) В Java объявить ещё один тип - это достаточно муторное занятие. Нужно создавать отдельный файл, ну и так далее. Но опыт показывает, что полезно создавать не мало больших типов, а много маленьких. А в Java это не удобно.
В Java публичный класс должен находиться в отдельном файле, а package-private и private классы не обязаны, могут жить в файле публичного класса, я часто это использую.
Я бы хотел, чтобы вы добавили возможность создавать типы в любом лексическом окружении. Это просто сделать, но это очень поможет программистам.
Java сто лет уже это умеет, вы можете объявлять классы внутри классов и внутри методов. Умеет ли язык автора - хз.
Я не сильно понял, что вы выиграли, отказавшись от наследования. С наследованием реализация была бы такой: у объекта есть указание на его класс, у класса таблица виртуальных функций. Вместо этого у вас каждый объект содержит поле-ссылку на родителя-делегата, по динамической памяти расход больше. Поэтому вот этот пассаж мне не ясен:
Компилятор берет всю тяжелую работу на себя и генерирует минимально необходимый RTTI (информация о типах во время выполнения), полностью упаковывая его в компактные метаданные во Flash-памяти. В ОЗУ под это не тратится ни одного байта.
Как это вы сделали, что у объекта нет информации о его классе?
Ещё хотел спросить: а что будет, если счётчик ссылок переполнится?
Не очень удобно развернуто отвечать в комментариях - ответы урезаны, это может вносить недопонимание.
Я планирую написать несколько статей где смогу развернуто ответить на вопросы как формируются HEAP данные класса , что лежит в метаданных Flash и тому подобное.
Также планирую рассказать как я работаю со ссылками и почему циклические ссылки и дефрагментация памяти не так критична в моей модели.
Забегая вперед приведу заголовок класса(для Thread и Timer заголовок расширен) в HEAP:
CLASS_HEAP_OFFSET__SIZE = 0x0000;2B-total size
CLASS_HEAP_OFFSET__LINK_CNTR = 0x0002;1B-Link counter
СLASS_HEAP_OFFSET__METADATA_ADDR = 0x0003;2B-flash address
Мета-данные в flash:
CLASS_META_OFFSET__CLASS_ID = 0x0000;1B-ИД типа класса
CLASS_META_OFFSET__PAIRS_QNT = 0x0001;1B-Количество пар
CLASS_META_OFFSET__PAIRS_TABLE = 0x0002;xB-таблица пар (ID интерфейса + количество методов)
Этого достаточно для выполнения динамической диспетчеризации вызовов. При наследовании классов боюсь пришлось бы хранить более большие таблицы (и возможно даже использовать RAM), плюс поиск реализации стал бы более затратным. Также усложняется процесс определения мертвого кода.
Счетчик ссылок дорастает до значения 255 и блокируется - память освободиться не сможет. Крайне маловероятно что это произойдет на 8 битах. Но можно будет это учесть - сделать исключение или если компилятор докажет - расширять счетчик до 16 бит.
Да, в комментах не очень удобно форматировать текст. Спасибо за ответ. На основе этого я бы сказал, что та цитата из статьи, к которой я прицепился, содержит неверную информацию: на RTTI в ОЗУ тратится два байта.
Насчёт наследования классов - нифига, принципиальной разницы с наследованием интерфейсов нет.
Заодно новый вопрос: а вам сильно помогает хранить размер объекта в ОЗУ ? Он же одинаков для всех объектов одного класса, и его можно получать, обратившись по METADATA_ADDR. Я ещё могу понять массивы, но предполагаю, что длина массива у вас хранится явно и отдельно.
"содержит неверную информацию" - я так упростил ответ, коряво вышло.
"Насчёт наследования классов - нифига, принципиальной разницы с наследованием интерфейсов нет." - не хочу сейчас об этом спорить, тем более я знаю единственный способ это доказать или опровергнуть - это реализовать. И может быть я к этому приду - так как такая доработка не поломает существующую модель. Сейчас меня вполне устраивает композиция, вроде-бы она сейчас даже в тренде.
"а вам сильно помогает хранить размер объекта в ОЗУ" - Это размер всего HEAP. У меня нет отдельного хранения размера блока выделенной памяти. В теории этот размер можно было бы хранить во flash, но я посчитал что там оптимальней.
Аналогично и массивы - есть нечто общее в заголовках. В нем есть размерность на базе которой вычисляется размер выделенной памяти.
Где вы видели динамическую память на 8 бит МК ?
У себя. И оно работает.
У vm5277 есть конечно ограничения - запрет на оператор new на чипах менее 256 байт - таким образом я блокирую возможность использования HEAP и динамической памяти. Все что от 256 может иметь DRAM и HEAP
И да, самые простые программы я запускаю даже на attiny13a, например:


Извиняюсь за банальный интерес, но что означает total time на скрине? Время цикла или что то другое?
Это вывод ассемблер сборщика. Т.е. время за которое avr ассемблер собрал из asm файла и его инклудов hex прошивку.
Вот пример 'hello' вывода работы компилятора (j8b->asm)

работы ассемблера (asm->hex)

И прошивальщика (hex->МК)

Конечно большие проекты будут собираться медленней. Но я еще вернусь к оптимизации процесса сборки - знаю что можно соптимизировать.
P.S. На первом скрине можно заметить, что total time заметно больше чем сумма работы компилятора - при сборке компилятор не показывает затраченное время на обработку автоматически подгружаемых библиотек из runtime. Но в total time это учитывается.
Помнится, сколько то лет назад здесь на хабре читал статью какого то чувака, который решил C# использовать для программирования под эмбендинг. Тоже как и в этой статье очень много было заделок на будущее и готовых идей. Если мне память не изменяет, была всего одна статья. C#, как и Java всегда шли плечом друг к другу. Так что дерзайте!
Спасибо. К вопросу выгорания - я этот проект веду почти каждый день уже как полтора года. Давно бы уже выгорел если бы мог. Причина в том, что я интроверт, и выгораю от социума, а в подобных проектах я отдыхаю, наверное это как рыбалка.
Да, безусловно, он может остаться никому не известным. Но причина в этом будет не техническая и не архитектурная - потому, что уже (даже в Альфе) существующие мои доработки доказывают реальность архитектуры и ее преимущества - по сути она закрывает сегмент рынка где особых конкурентов просто нет. Вопрос только в сообществе - удастся ли мне набрать критическую массу этого сообщества.
кажется, что в статье нет ответа на главный вопрос - ЗАЧЕМ все это нужно. абстракция от железа и многопоточность на уровне языка? а.. зачем? если есть ртос? это все красивые идеи, но жизнеспособные только в стране розовых единорогов (имхо). не вижу необходимости в подобном подходе и инструментарии даже на 32бит гигагерцовых процессорах с десятками мб озу. а у вас речь про 8бит с десятками КИЛОБАЙТ. попробуйте переубедить меня
Я не знаю чем Вы занимаетесь, чтобы Вас переубедить. Это инструмент - достоинства которого я описал. Можно еще почитать https://github.com/w5277c/vm5277/blob/main/FAQ.md для дополнительной информации и посетить сайт-визитку https://vm5277.ru/
На самом деле этот вопрос очень прост. У него ровно такие-же ответы как и на вопрос 'зачем писать на Java когда есть Си' - не думаю, что я смогу написать ответ лучше чем уже существующие ответы.
Лично от себя приведу пример:
У меня есть свой проект умного дома, со значительным набором своих устройств (датчики, управление, шлюзы и прочее).
Все прошивки реализованы на чистом ассемблере на базе моей RTOS (core5277). vm5277 - это следующий этап развития core5277 - так как ему не хватало функционала для легкого описания прикладной (бизнес) логики и не хватало переносимости.
Когда мы говорим об одной конкретной прошивке с простым функуионалом - да тратить силы на какое-то стороннее решение (да и еще от него зависеть) - это глупо.
Но как только у вас появляются задачи где нужно реализовать кучу бизнес логики а потом, вполне вероятно ситуация переноса прошивки на другие чипы - вот здесь удобство vm5277 сильно перевешивает нативный кодинг даже на Си.
Плюс, мой язык дает безопасность и легкую читабельность, которой нет в Си.
Я предлагаю инструмент который в будущем позволит Вам писать бизнес логику гораздо с меньшими трудозатратами, с легко читаемым синтаксисом, с существенно меньшим порогом вхождения в embedded, с большей безопасностью итогового кода и с легкой переносимостью на другие чипы. Если для Вас эти характеристики не существенны - то да, Вам мой проект не нужен. А вот бизнесу такой проект нужен, так как существенно сокращает цену разработки и позволяет нанимать менее дорогостоящих специалистов.
да, именно этот вопрос. зачем писать на java (которая мне нравится на РС) если есть С? каких средств языка вам не хватает в эмбеддед С? переносимость (что кстати спорно - С - весьма переносим) и низкий порог входа это справедливо при умалчиваемых вами допущениях, что низкоуровневый слой для данной платформы уже кем-то написан. а с этим "все очень сложно" даже во вселенной С с ее терабайтами исх текстов: _нормальных низкоуровневых библиотек днем с огнем поискать. и да, по указанным выше ссылкам я побродил ранее.
кстати почему вы ориентируетесь при написании низкого уровня на ассемблер? кажется, что это многих отпугнет сложностью самостоятельной доработки библиотек низкого уровня. почему не С? насколько я помню семейство АВР (из 8бит ников) как раз и разрабатывалось так, чтоб на нем максимально эффективно работали программы на С
а про "безопасность" С- кода отвечу бородатым анекдотом:
-доктор, помогите! когда я ВОТ ТАК делаю - мне больно.
-голубчик! а вы ТАК - не делайте!
Судя по вашим аргументам, наше обсуждение вышло из технического русла и перешло в область идеологического фанатизма.
Ваши вопросы и тезисы («зачем всё это нужно», «перенубедите меня», «всё сложно») имеют исключительно холиварный характер. Вы требуете от меня универсальных ответов на субъективные вопросы, на которые embedded-сообщество спорит десятилетиями.
Дискутировать в таком ключе непродуктивно. Я не планирую тратить силы на то, чтобы переубеждать лично вас или доказывать очевидную ценность HAL, переносимости кода и безопасности памяти для бизнеса. Все технические ответы, исходный код Альфы и примеры работы на реальном железе (включая WS2812B и экраны) уже выложены в моем репозитории. Кому интересно - тот изучает и пробует. Всего вам доброго
vm5277: Java-синтаксис и ООП для 8-бит МК без оверхеда