Русский
Русский
English
Статистика
Реклама

Clr

.NET nanoFramework платформа для разработки приложений на C для микроконтроллеров

25.03.2021 22:22:00 | Автор: admin
nanoframework

.NET nanoFramework это бесплатная платформа с открытым исходным кодом, основанная на .NET и предназначена для малых встраиваемых устройств, микроконтроллеров. С ее помощью можно разрабатывать различные устройства для Интернета вещей, носимые устройства, научные приборы, робототехнические устройства, можно создавать прототипы и даже использовать на промышленном оборудование.

.NET nanoFramework является малой версией большого .NET Framework предназначенного для настольных систем. Разработка приложений ведется на языке C# в среде разработки Visual Studio. Сама платформа является исполнительной средой .NET кода, это позволяет абстрагироваться от аппаратного обеспечения и дает возможность переносить программный код с одного микроконтроллера на другой, который тоже поддерживает .NET nanoFramework. Программный код на C# для настольных систем, без изменений или с небольшой адаптацией (необходимо помнить про малый объем оперативной памяти) исполнится на микроконтроллере. Благодаря этому, разработчики на .NET с минимальными знаниями в области микроэлектроники смогут разрабатывать различные устройства на .NET nanoFramework.

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

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

Подход использования языков программирования высокого уровня с сочетанием высокой абстракцией аппаратного обеспечения существенно расширяет круг потенциальных разработчиков и сильно снижает стоимость разработки устройства с точки зрения затрат человеко-часов и отсутствия необходимости долгого процесса обучения разработчика.

Особенности .NET nanoFramework:

  • Может работать на 32- и 64-разрядных микроконтроллерах ARM, с наличием всего 256 КБ флэш-памяти и 64 КБ ОЗУ.
  • Работает нативно на чипе, в настоящее время поддерживаются устройства ARM Cortex-M и ESP32.
  • Поддерживает самые распространенные интерфейсы такие как :GPIO, UART, SPI, I2C, USB, networking.
  • Обеспечивает встроенную поддержку многопоточности.
  • Включает функции управления электропитанием для обеспечения энергоэффективности, например, устройств работающих от аккумуляторных батарей.
  • Поддерживает совмещение управляемого кода на C# и неуправляемого кода на C/C++ в одном проекте.
  • Автоматическая сборка мусора благодаря сборщику мусора.

В сравнение с другими платформами:

  • Доступен интерактивный отладчик при запуске кода на самом устройстве с точками останова.
  • Есть развитая и бесплатная среда программирования с Microsoft Visual Studio.
  • Поддержка большого количества недорогих плат от различных производителей, включая: платы Discovery и Nucleo от ST Microelectronics, Quail от Mikrobus, Netduino от Wilderness Labs, ESP32 DevKit C, Texas Instruments CC3220 Launchpad, CC1352 Launchpad и NXP MIMXRT1060-EVK.
  • Легко переносится на другие аппаратные платформы и устройства на ОС RTOS .В настоящее время совместимость обеспечивается в соответствие с CMSIS и ESP32 FreeRTOS.
  • Полностью бесплатное решение с открытым исходным кодом, никаких скрытых лицензионных отчислений. От основных компонентов до утилит, используемых для создания, развертывания, отладки и компонентов IDE.


Предыстория


Вначале было Слово и было это Слово .NET Micro Framework от компании Micrsoft. До появления .NET nanoFramework, в Microsoft любительский проект перерос в серьезную платформу .NET Micro Framework, которая быстро завоевала популярность на американском рынке. Такая компания GHI Electronics с 2008 года, построила весь свой бизнес на разработке микроконтроллеров и решений на базе .NET Micro Framework. В портфолио GHI Electronics были небольшие микроконтроллеры в стиле Arduino FEZ Domino и весьма производительные с несколькими мегабайтами ОЗУ (для микроконтроллеров это весьма круто).

Микроконтроллеры компании GHI Electronics

GHI Electronics Modules

Устройства могли работать практически с любой периферией, была поддержка стека TCP/IP, WiFI, обновления по воздуху. Была даже ограниченная реализация СУБД SQLite, поддержка USB Host и USB Client. Не трудно понять что компания быстро смогла себе сделать имя, стать основным разработчикам решений на .NET Micro Framework, продукцию которой постановляют во все страны мира.

В 2014 г. в проекте Школьный звонок на .NET Micro Framework с удаленным управлением мною использовалась плата FEZ Domino от GHI Electronics. Так же было и множество других проектов таких как Netduino.

FEZ Domino

FEZ Domino

В октябре 2015 года на GitHub был опубликован релиз .NET Micro Framework v4.4, этот релиз оказался последним. Компании Micrsoft отказалась дальше развивать платформу, с этого момента начинает свою историю проект nanoFramework (с 2017 года), в основе которого кодовая база .NET Micro Framework. Многие основные библиотеки были полностью переписаны, некоторые перенесены как есть, было проведено множество чисток и улучшений кода. Энтузиасты встраиваемых систем, гики увлеченные программированием, возродили платформу!

Работа над платформой ведется в основном в свободное время. Некоторые из основных участников работают в компаниях, которые активно спонсируют .NET nanoFramework и предлагают свои рабочие часы для продвижения проекта. Если вы используете .NET nanoFramework как часть своего продукта или просто хотите его поддержать, можете сделать пожертвование. Это позволяет оплачивать затраты на инфраструктуру и уделять больше времени на реализацию проекта.


Архитектура платформы


Платформа включает в себя уменьшенную версию .NET Common Language Runtime (CLR) и подмножество библиотек базовых классов .NET вместе с наиболее распространенными API-интерфейсами, включенными в универсальную платформу Windows (UWP). В текущей реализации, .NET nanoFramework работает поверх ChibiOS которая поддерживается, некоторыми платами ST Microelectronics, Espressif ESP32, Texas InstrumentsCC3220 Launchpad,CC1352 Launchpad и NXP MIMXRT1060-EVK. Разработка ведется в Microsoft Visual Studio или Visual Studio Code, отладка производится непосредственно на устройстве в интерактивном режиме.

Общая архитектура


Для того чтобы эффективно писать код на C # на микроконтроллере, необходимо нужно понимать (на высоком уровне), как это работает. Весь код в микроконтроллере делится на 4 логических компонента, как показано ниже:

nanoframework architecture

На самом высоком уровне находится ваше приложение на C #, которое необходимо запускать на MCU. Ниже располагается уровень CLR, исполнительная среда для нашей программы. Загрузчик базовый компонент любого микроконтроллера, запускает среду CLR при включении MCU. Наконец, на самом низком уровне у нас есть MCU.

Для использования .NET nanoFramework необходимо загрузить среду nanoCLR на микроконтроллер. После этого приложение на C#, скомпилированное в виде бинарного файла, можно загружать на микроконтроллер.

Схема архитектуры .NET nanoFramework


nanoframework architecture

nanoCLR базируется на слое аппаратной абстракции (HAL). HAL предоставляет абстракцию устройств и стандартизует методы и функции работы с устройствами. Это позволяет использовать наборы функций которые одинаковы доступны на уровне абстракции платформы (PAL) и конкретных драйверов. Среда nanoCLR построена на PAL и содержит некоторые ключевые библиотеки, такие как mscorlib (System и несколько других пространств имен), которые всегда используются. Модульность .NET nanoFramework позволяет добавлять пространства имен (namespaces) и классы, связанные с nanoCLR.


ChibiOS


ChibiOS/RT компактная многозадачная операционная система реального времени (ОСРВ). Предназначена для встраиваемых приложений, работающих в реальном времени. Эта ОСРВ отличается высокой мобильностью, компактными размерами и, главным образом имеет свою собственную уникальную архитектуру, которая как никак подходит для быстрого и эффективного переключения контекста.

Основные характеристики:

  • Эффективное и портативное ядро.
  • Лучшая в своем классе реализация переключения контекста.
  • Множество поддерживаемых платформ.
  • Статичная архитектура все статически выделяется во время компиляции.
  • Динамические расширения динамические объекты поддерживаются как дополнительный слой надстройки статичного ядра.
  • Богатый набор примитивов для ОСРВ: потоки (threads), виртуальные таймера (virtual timers), семафоры (semaphores), мьютексы (mutexes), переменные условия/синхронизации (condition variables), сообщения (messages), очереди (queues), флаги событий (event flags) и почтовый ящик (mailboxes).
  • Поддержка алгоритма наследования для мьютексов.
  • HAL-компонент поддержки различных абстрактных драйверов устройств: порт, последовательный порт, ADC, CAN, I2C, MAC, MMC, PWM, SPI, UART, USB, USB-CDC.
  • Поддержка внешних компонентов uIP, lwIP, FatFs.
  • Инструментарий для отладки ОСРВ

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

  • ARM7, ARM9
  • Cortex-M0, -M0+, -M3, -M4, -M7
  • PPC e200zX
  • STM8
  • MSP430
  • AVR
  • x86
  • PIC32

Области применения ChibiOs/RT:

  • Автомобильная электроника.
  • Робототехника и промышленная автоматика.
  • Бытовая электроника.
  • Системы управления электроэнергией.
  • DIY.

ChibiOS/RT также был портирована на Raspberry Pi, и были реализованы следующие драйверы устройств: порт (GPIO), Seral, GPT (универсальный таймер), I2C, SPI и PWM.


Поддерживаемые устройства


Поддерживаемые устройства делятся на две категории: основные платы и поддерживаемые сообществом.

Основные платы:

  • ESP32 WROOM-32, ESP32 WROOM-32D, ESP32 WROOM-32U, ESP32 SOLO-1
  • ESP-WROVER-KIT, ESP32 WROVER-B, ESP32 WROVER-IB
  • STM32NUCLEO-F091RC
  • STM32F429IDISCOVERY
  • STM32F769IDISCOVERY
  • OrgPal PalThree
  • CC1352R1_LAUNCHXL
  • CC3220SF_LAUNCHXL
  • MIMXRT1060 Evalboard
  • Netduino N3 WiFi

Платы поддерживаемые сообществом:

  • ESP32 ULX3S
  • STM32NUCLEO144-F746ZG
  • STM32F4DISCOVERY
  • TI CC1352P1 LAUNCHXL
  • GHI FEZ cerb40 nf
  • I2M Electron nf
  • I2M Oxygen
  • ST Nucleo 144 f439zi
  • ST Nucleo 64 f401re/f411re nf
  • STM NUCLEO144 F439ZI board
  • QUAIL


Пример программы


Примеры кода представлены в разделе nanoframework/Samples. Рассмотрим базовый пример, Blinky пример программы позволяющей мигать встроенным светодиодом на плате ESP32-WROOM:

using System.Device.Gpio;using System;using System.Threading;namespace Blinky{public class Program    {        private static GpioController s_GpioController;        public static void Main()        {            s_GpioController = new GpioController();            // ESP32 DevKit: 4 is a valid GPIO pin in, some boards like Xiuxin ESP32 may require GPIO Pin 2 instead.            GpioPin led = s_GpioController.OpenPin(4,PinMode.Output);            led.Write(PinValue.Low);            while (true)            {                led.Toggle();                Thread.Sleep(125);                led.Toggle();                Thread.Sleep(125);                led.Toggle();                Thread.Sleep(125);                led.Toggle();                Thread.Sleep(525);            }        }            }}

Программный код простой и понятный. Не требуется никаких детальных знаний аппаратной платформы, все что нужно знать, это распиновку платы. Вначале кода объявляется глобальный объект GpioController, затем устанавливается контакт с номером 4 на вывод, после этого в коде идет простое инверсное переключение состояния. Это все что требуется знать.


Что сейчас доступно из коробки?


Аппаратные интерфейсы:

  • Windows.Devices.WiFi работа с Wi-Fi сетью.
  • nanoFramework.Devices.Can работа с CAN шиной. CAN (Controller Area Network) стандарт промышленной сети, ориентированный, прежде всего, на объединение в единую сеть различных исполнительных устройств и датчиков, заменяет устаревшую шину RS 485. Используется в АСУ ТП, на заводах и фабриках.
  • 1-Wire 1-Wire интерфейс, используется для подключения одного/нескольких температурных датчиков DS18B20.
  • Windows.Devices.I2c, System.Device.I2c I2C шина для подключения нескольких устройств.
  • Windows.Devices.Spi SPI шина.
  • Windows.Devices.Adc аналого-цифровой преобразователь (АЦП).
  • Windows.Devices.Pwm широтно-импульсная модуляция (ШИМ).
  • System.Devices.Dac - цифро-аналоговый преобразователь (ЦАП).

Классы:

  • Windows.Devices.SerialCommunication работа с последовательным интерфейсом.
  • MQTT MQTT клиент, порт популярной библиотеки M2Mqtt. Библиотека предназначена для отправки коротких сообщений, используется для Интернета вещей и M2M взаимодействия.
  • System.Net.Http.Server и System.Net.Http.Client готовые классы Web-сервера и Web-клиента с поддержкой TLS/SSL.
  • Json работа с данными в формате Json.
  • nanoFramework.Graphics библиотека работы с отображения графических примитивов на LCD-TFT дисплеях.
  • System.Collections коллекции объектов.
  • Discord bot реализация Discord бота.
  • Json Serializer and Deserializer Json сериализацияя/десериализация.

Сетевые протоколы:

  • AMQP.Net Lite Облегченная версия открытого протокола AMQP 1.0 для передачи сообщений между компонентами системы. Основная идея заключается в том, что отдельные подсистемы (или независимые приложения) могут обмениваться произвольным образом сообщениями через AMQP-брокера, который осуществляет маршрутизацию, гарантирует доставку, распределяет потоки данных, предоставляет подписку на нужные типы сообщений. Используется в инфраструктуре Azure, поддерживает шифрование TLS.
  • SNTP протокол синхронизации времени по компьютерной сети.


Библиотеки классов


В таблице представлена общая организация библиотек классов .NET nanoFramework. Приведенные ниже примеры относятся к ChibiOS (которая в настоящее время является эталонной реализацией .NET nanoFramework):

Библиотека класса Название Nuget пакета
Base Class Library (also know as mscorlib) nanoFramework.CoreLibrary
nanoFramework.Hardware.Esp32 nanoFramework.Hardware.Esp32
nanoFramework.Runtime.Events nanoFramework.Runtime.Events
nanoFramework.Runtime.Native nanoFramework.Runtime.Native
nanoFramework.Runtime.Sntp nanoFramework.Runtime.Sntp
Windows.Devices.Adc nanoFramework.Windows.Devices.Adc
Windows.Devices.I2c nanoFramework.Windows.Devices.I2c
Windows.Device.Gpio nanoFramework.Windows.Devices.Gpio
Windows.Devices.Pwm nanoFramework.Windows.Devices.Pwm
Windows.Devices.SerialCommunication nanoFramework.Windows.Devices.SerialCommunication
Windows.Devices.Spi nanoFramework.Windows.Devices.Spi
Windows.Devices.WiFi nanoFramework.Windows.Devices.WiFi
Windows.Networking.Sockets nanoFramework.Windows.Networking.Sockets
Windows.Storage nanoFramework.Windows.Storage
Windows.Storage.Streams nanoFramework.Windows.Storage.Streams
System.Net nanoFramework.Windows.System.Net

Все дополнительные пакеты добавляются с помощью системы Nuget, как это принято в .NET Core.


Unit-тестирование


nanoframework unit test architecture

Тестирование настольного приложения на рабочей станции не вызывает никаких проблем, но все обстоит иначе, если необходимо тестировать приложение для микроконтроллера. Исполнительная среда должна быть эквивалентна по характеристикам микроконтроллеру. В Unit тестирование кода на C#, используется концепция Адаптера (Adapter). Для тестовой платформы Visual Studio (vstest) был разработан специальный компонент nanoFramework.TestAdapter. В нем реализовано, два интерфейса для детального описания конфигурации и третий для описания специфических параметров, таких как time out, в зависимости от целевой среды исполнения, на реальном оборудование или в Win32 nanoCLR. Механизм проведения тестов на Win32 nanoCLR и реальном оборудовании, одинаков. Единственное отличие это консоль вывода, которая в случае реального оборудования отправляет данные в порт отладки.

Один из интерфейсов называется ITestDiscoverer, который используется Visual Studio для сбора возможных тестов. vstest вызовет адаптер для любой запущенной сборки и передаст бинарный файл dll или exe, соответствующую определенному условию сборки (пока у нас нет TFM, и мы используем небольшой хак и основной .NET Framework 4.0 ). Затем nanoFramework TestAdapter анализирует каталоги, чтобы найти nfproj, анализируя файлы cs, глядя на определенные атрибуты Test, определенные для nanoFramework. На основе этого составляется список, который передается обратно.

Этот хак выполняется с помощью файла с расширением .runsettings с необходимым минимумов элементов ( для запуска приложения в Win32 nanoCLR, параметр IsRealHardware необходимо выставить в false, в случае реального устройства true):

nanoframework unit test

Когда выполняется сборка проекта отрабатывает триггер интерфейс ITestExecutor. В случае, если вы работаете в контексте Visual Studio, передается список тестов (индивидуальный или полный список), и именно здесь запускается nanoFramework.nanoCLR.Win32.exe как процесс, передающий nanoFramework.UnitTestLauncher.pe, mscorlib.pe, nanoFramework.TestFramework.pe и, конечно же, тестовую библиотеку для исполняемого файла.

nanoFramework Unit Test загрузит тестовую сборку и выполнит тесты с начиная с первого Setup, затем TestMethod и, наконец, тесты Cleanup.

Выходные данные этого процесса перенаправляются в TestAdapter, которые затем разбираются. Важное примечание: UnitTestLauncher необходимо собрать и развернуть при отладке! В противном случае он никогда не сможет ничего вывести в консоль. Чтобы запустить тест на реальном оборудование, нужно просто добавить строку ввода в файл runsettings, как указано выше.

По окончании выполнения тестов, возвращается статусы. Простой строковый вывод со статусом теста, имени метода и времени его выполнения и/или исключение. Тест пройден: MethodName, 1234 или Test failed: MethodName, подробное исключение. Это передается обратно в vstest, а затем отображается в Visual Studio.

Для Unit-тестирования необходимо в проект добавить NuGet пакет nanoFramework.TestFramework. Или использовать готовый проект для Unit-тестирования в Visual Studio, это самый простой способ! Он автоматически добавит в проект NuGet и .runsettings.


Лицензирование


Весь исходный код .NET nanoFramework распространяется по лицензией MIT, включая nf-interpreter, классы библиотек, расширение Visual Studio и все сопутствующие утилиты.

Немного сложнее обстоит с лицензированием ChibiOS. Для персональных проектов ChibiOS бесплатна, при этом свой код не обязательно открыто публиковать, это может быть закрытый проект.

Для коммерческого использования ChibiOS, необходимо приобретать лицензию. Так же можно лицензировать отдельные компоненты.

Но если количество выпущенных коммерческих устройств с ChibiOS не превышает 500 ядер, то оплачивать лицензию не требуется. Свыше этого объема приобретаются пакеты лицензий на 500, 1000, 5000 ядер или неограниченно. При бесплатном коммерческом использование некоторые функции недоступны и в своем продукте необходимо реализовать рекламу ChibiOS.

Управляемые приложения managed apps (C#) запущенные на .NET nanoFramework не компилируются и не собираются ChibiOS, а интерпретируются на лету. Поэтому рассматриваются как отдельный компонент от встроенного ПО. Таким образом, схема лицензирования ChibiOS не распространяется на приложения C#, и не зависит от условий лицензирования ChibiOS.


Использование в промышленном сфере


Американская компания OrgPal.Iot специализируется на аппаратных и программных решениях для Интернета вещей (IOT), которые осуществляют сбор данных телеметрии для отдаленнейшего анализа, управление инфраструктурой через частное облако. Компания предоставляет конечные устройства и шлюзы для передачи данных на рабочие станции, сервера и мобильные устройства. Решения совместимы с Azure IoT.

Основные направление компании это мониторинг инфраструктуры:

  • Промышленного производства
  • Нефтяных месторождений
  • Панелей солнечных электростанций
  • Систем управления электропитанием

Одно из разработанных устройств компании это PalThree. PalThree сертифицированное устройство Azure IoT Ready Gateway & Edge Point для сбора данных и телеметрии с последующей передачей в облако Azure IoT. На борту большой набор входных интерфейсов для получения данных о технологических процессах. Устройство основано на STM32 ARM 4 и ARM 7, поставляется в двух вариантах с микроконтроллерами STM32F469x и STM32F769x, с 1 МБ SDRAM на плате, флэш-памятью SPI и QSPI. Программное обеспечение основано на .NET nanoFramework, с ChibiOS для STM32.

Как это работает

nanoframework palthree sensors cloud

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

Цистерны для хранения нефтегазовых продуктов
nanoframework oil tank palthree

Шкаф с PalThree
nanoframework palthree close up


Web-сервер с поддержкой: REST api, многопоточности, параметров в URL запросе, статических файлов.


Специально для .NET nanoFramework, Laurent Ellerbach разработал библиотеку nanoFramework.WebServer, которая по своей сути является упрощенной версией ASP.NET.

Возможности Web-сервера:

  • Обработка многопоточных запросов
  • Хранение статических файлов на любом носителе
  • Обработка параметров в URL запросе
  • Возможность одновременной работы нескольких веб-серверов
  • Поддержка команд GET/PUT и любых другие
  • Поддержка любого типа заголовка http-запроса
  • Поддерживает контент в POST запросе
  • Доступно использование контроллеров и маршрутизации
  • Хелперы для возврата кода ошибок, для REST API
  • Поддержка HTTPS
  • Декодирование/кодирование URL

Ограничение: не поддерживает компрессию ZIP в запросе и ответе.

Для использования Web-сервера необходимо просто указать порт и добавить обработчик запросов:

using (WebServer server = new WebServer(80, HttpProtocol.Http){    // Add a handler for commands that are received by the server.    server.CommandReceived += ServerCommandReceived;    // Start the server.    server.Start();    Thread.Sleep(Timeout.Infinite);}

Так же, можно передать контроллер, например ControllerTest, и будет использоваться декоратор для маршрутизации и методов:

using (WebServer server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(ControllerPerson), typeof(ControllerTest) })){    // Start the server.    server.Start();    Thread.Sleep(Timeout.Infinite);}

В следующем примере, определяется маршрут test и т.д., в котором определяется метод GET, и test/any:

public class ControllerTest{    [Route("test"), Route("Test2"), Route("tEst42"), Route("TEST")]    [CaseSensitive]    [Method("GET")]    public void RoutePostTest(WebServerEventArgs e)    {        string route = $"The route asked is {e.Context.Request.RawUrl.TrimStart('/').Split('/')[0]}";        e.Context.Response.ContentType = "text/plain";        WebServer.OutPutStream(e.Context.Response, route);    }    [Route("test/any")]    public void RouteAnyTest(WebServerEventArgs e)    {        WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);    }}

Функция RoutePostTest будет вызываться каждый раз, когда вызываемый URL-адрес будет test, Test2, tEst42 или TEST, URL-адрес может быть с параметрами и методом GET. RouteAnyTest вызывается всякий раз, когда URL-адрес является test/any, независимо от метода. По умолчанию маршруты не чувствительны к регистру, и атрибут должен быть в нижнем регистре.

Если вы хотите использовать маршруты с учетом регистра, как в предыдущем примере, используйте атрибут CaseSensitive. Как и в предыдущем примере, вы должны написать маршрут так, как вы хотите, чтобы на него ответили.

Cooming Soon


В продолжение к публикации, будет практическая работа с загрузкой CLR на ESP32 и Nucleo, с написанием первой программы на C#.

Страница проекта .NET nanoFramework
Подробнее..

Recovery mode Лучший язык программирования

24.02.2021 10:16:44 | Автор: admin

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

Принципы отбора

Экосистем много, начинать с выбранных достоинств какой-то одной глупо, поэтому действовать будем методом исключения по ясным пунктам. Все сравнения опираются на объективные бенчмарки.

Поддерживаемость

Прежде всего, язык должен быть достаточно мейнстримным, чтобы проект на нем был поддерживаемым. Сразу выбрасываем за борт всю экзотику и функциональщину вроде Haskell, Elixir, Nim, Erlang умирающий Ruby туда же. По этой же причине отбрасываем и всевозможные языки закрытых (Swift) и тем более окукленных по паспорту (1C например) экосистем.

Типизация

Общая практика в индустрии показывает что слабая типизация однозначно вредит читаемости, поддерживаемости и порождает большое количество ошибок, поэтому выкидываем JavaScript и PHP. Далее, замечаем что динамическая типизация ухудшает скорость работы, а варианты компиляции традиционно динамических языков, отличаются плохой поддерживаемостью, и выглядят скорее как извращение приделанное сбоку костылями, поэтому за бортом остаются и строго-типизированные, но динамические Python и TypeScript.

В сухом остатке

У нас остались мейнстримные, строго-статически-типизированные C#, Java, C++, C и восходящие в последнее время к ним Kotlin, Go и Rust. Не каждый пожелает (а еще меньше смогут) использовать C++ в большом проекте, поэтому, несмотря на лучшие пока показатели по скорости, отложим его вместе с более низкоуровневым Си для узких мест.

Java vs C#

C# и Java близнецы. Экосистемы обоих языков на данный момент открыты и очень развиты. Java здесь выходит вперед за счет большего количества инструментов, библиотек, конкуренции и вариантов выбора. Но C# более стройный синтаксически, в нем исправлены известные проблемы Java (слабые дженерики пропадающие на этапе компиляции, отсутствие пользовательских значимых типов на стеке, сочетаемость значимых и ссылочных типов как List). Кроме того у .NET в целом лучшие показатели по скорости и памяти.

Kotlin

Долгое время C# выглядел практически идеальным по кроссплатформености, высокоуровневости абстракций и скорости приближающейся местами к С++. Однако практика с Kotlin показала, что он превосходит C# и по концепциям, и по синтаксической стройности. И вот почему. Если C# идет по пути добавления в ядро языка все новых и новых абстракций и ключевых слов, то в Kotlin весь синтаксический "сахар" базируется на встраиваемых лямбдах и функциях области видимости и вынесен в стандартную библиотеку. В чем здесь преимущество? Почти любую фичу, казалось бы языка, в Kotlin можно прочитать в стандартной библиотеке и понять как любой другой код. Kotlin, правда, немного уступает C# по скорости, но лишь потому что компилируется в байт-код Java.

Go?

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

Rust?

Начать следует с того что у Rust порог входа выше чем у C++, уже потому что на C++ сразу можно писать как на любом другом высокоуровневом ЯП, предоставив стандартной библиотеке разруливать работу с памятью. Rust же не даст спрятаться от решения вопросов безопасности и размещения в памяти и из-за этой необходимости, он (внезапно!) превращается в достаточно низкоуровневый язык, сравнимый скорее даже с Си, чем с C++.

Вывод

Мы по возможности непредвзято рассмотрели объективные факторы способствующие индустриальной разработке. Вывод из них все же неизбежно будет субъективен. Поэтому привествуются альтернативные выводы!
Итак, по моему мнению, на текущий момент, для написания большинства кроссплатформенных приложений удобнее всего использовать Kotlin, дополняя его C++ в тех случаях когда требуется скорость. Kotlin прекрасно подходит для бекендов, язык экосистемы Android по умолчанию, без проблем компилируется в JS и WebAssembly для браузеров, с небольшими приседаниями может быть использован для iOS, а с помощью jpackage легко подготовить самодостаточный исполняемый файл для Windows, macOS, Linux в "родном" формате.

Подробнее..

Хорошо ли вы помните nullable value types? Заглядываем под капот

30.10.2020 16:06:21 | Автор: admin
image1.png

В последнее время модной темой стали nullable reference types. Однако старые добрые nullable value types никуда не делись и всё так же активно используются. Хорошо ли вы помните нюансы работы с ними? Предлагаю освежить или проверить свои знания, ознакомившись с этой статьёй. Примеры кода на C# и IL, обращения к спецификации CLI и коду CoreCLR прилагаются. Начать предлагаю с интересной задачки.

Примечание. Если вас интересуют nullable reference types, можете познакомиться с несколькими статьями моих коллег: "Nullable Reference типы в C# 8.0 и статический анализ", "Nullable Reference не защищают, и вот доказательства".

Посмотрите на пример кода ниже, и ответьте, что будет выведено в консоль. И, что не менее важно, почему. Только давайте сразу договоримся, что вы ответите, как есть: без подсказок компиляторов, документации, вычитывания литературы или чего-то подобного. :)

static void NullableTest(){  int? a = null;  object aObj = a;  int? b = new int?();  object bObj = b;  Console.WriteLine(Object.ReferenceEquals(aObj, bObj)); // True or False?}

image2.png

Что ж, давайте немного порассуждаем. Возьмём несколько основных направлений мысли, которые, как мне кажется, могут возникнуть.

1. Исходим из того, что int? ссылочный тип.

Давайте рассуждать так, что int? это ссылочный тип. В таком случае в а будет записано значение null, оно же будет записано и в aObj после присвоения. В b будет записана ссылка на какой-то объект. Она также будет записана и в bObj после присвоения. В итоге, Object.ReferenceEquals примет в качестве аргументов значение null и ненулевую ссылку на объект, так что

Всё очевидно, ответ False!

2. Исходим из того, что int? значимый тип.

А может быть вы сомневаетесь, что int? ссылочный тип? И вы уверены в этом, несмотря на выражение int? a = null? Что ж, давайте зайдём с другой стороны и будем отталкиваться от того, что int? значимый тип.

В таком случае выражение int? a = null выглядит немного странно, но предположим, что это опять в C# сахара сверху насыпали. Получается, что a хранит какой-то объект. b тоже хранит какой-то объект. При инициализации переменных aObj и bObj будет произведена упаковка объектов, хранимых в a и b, в результате чего в aObj и в bObj будут записаны разные ссылки. Получается, что Object.ReferenceEquals в качестве аргументов принимает ссылки на разные объекты, следовательно

Всё очевидно, ответ False!

3. Исходим из того, что здесь используется Nullable<T>.

Допустим, варианты выше вам не понравились. Потому что вы отлично знаете, что никакого int? на самом деле нет, а есть значимый тип Nullable<T>, и в данном случае будет использован Nullable<int>. Также вы понимаете, что на самом деле в a и b будут одинаковые объекты. При этом вы не забыли, что при записи значений в aObj и в bObj произойдёт упаковка, и в итоге будут получены ссылки на разные объекты. Так как Object.ReferenceEquals принимает ссылки на разные объекты, то

Всё очевидно, ответ False!

4. ;)

Для тех, кто отталкивался от значимых типов, если у вас вдруг закрались какие-то сомнения про сравнение ссылок, то можно посмотреть документацию по Object.ReferenceEquals на docs.microsoft.com. В частности, там тоже затрагивают тему значимых типов и упаковки/распаковки. Правда, там описывается кейс, когда экземпляры значимых типов передаются непосредственно в метод, мы же упаковку вынесли отдельно, но суть та же.

When comparing value types. If objA and objB are value types, they are boxed before they are passed to the ReferenceEquals method. This means that if both objA and objB represent the same instance of a value type, the ReferenceEquals method nevertheless returns false, as the following example shows.

Казалось бы, здесь статью можно и закончить, вот только правильный ответ True.

Что ж, давайте разбираться.

Разбираемся


Есть два пути простой и интересный.

Простой путь


int? это Nullable<int>. Открываем документацию по Nullable<T>, где смотрим раздел "Boxing and Unboxing". В принципе, на этом всё поведение там описано. Но если хочется побольше деталей, приглашаю на интересный путь. ;)

Интересный путь


На этой тропинке нам будет недостаточно документации. Она описывает поведение, но не отвечает на вопрос 'почему'?

Что такое на самом деле int? и null в соответствующем контексте? Почему это работает так? В IL коде используются разные команды или нет? Отличается поведение на уровне CLR? Ещё какая-то магия?

Начнём с разбора сущности int?, чтобы вспомнить основы, и постепенно дойдём до разбора первоначального кейса. Так как C# язык достаточно "приторный", периодически будем обращаться к IL коду, чтобы смотреть в суть вещей (да, документация по C# не наш путь сегодня).

int?, Nullable<T>


Здесь рассмотрим основы nullable value types в принципе (что из себя представляет, во что компилируются в IL и т.п.). Ответ на вопрос из задания рассмотрен в следующем разделе.

Рассмотрим фрагмент кода.

int? aVal = null;int? bVal = new int?();Nullable<int> cVal = null;Nullable<int> dVal = new Nullable<int>();

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

.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,              valuetype [System.Runtime]System.Nullable`1<int32> V_1,              valuetype [System.Runtime]System.Nullable`1<int32> V_2,              valuetype [System.Runtime]System.Nullable`1<int32> V_3)// aValldloca.s V_0initobj  valuetype [System.Runtime]System.Nullable`1<int32>// bValldloca.s V_1initobj  valuetype [System.Runtime]System.Nullable`1<int32>// cValldloca.s V_2initobj  valuetype [System.Runtime]System.Nullable`1<int32>// dValldloca.s V_3initobj  valuetype [System.Runtime]System.Nullable`1<int32>

Как видно, в C# всё от души сдобрено синтаксическим сахаром, чтобы нам с вами жилось лучше, по факту же:

  • int? значимый тип.
  • int? то же самое, что Nullable<int>. В IL коде идёт работа с Nullable<int32>.
  • int? aVal = null то же самое, что Nullable<int> aVal = new Nullable<int>(). В IL это разворачивается в инструкцию initobj, которая выполняет инициализацию по умолчанию по загруженному адресу.

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

int? aVal = 62;

С инициализацией по умолчанию мы разобрались соответствующий IL код мы видели выше. Что же происходит здесь, когда мы хотим проинициализировать aVal значением 62?

Взглянем на IL код:

.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0)ldloca.s   V_1ldc.i4.s   62call       instance void valuetype            [System.Runtime]System.Nullable`1<int32>::.ctor(!0)

Опять же, ничего сложного на evaluation stack загружается адрес aVal, а также значение 62, после чего вызывается конструктор с сигнатурой Nullable<T>(T). То есть два следующих выражения будут полностью идентичны:

int? aVal = 62;Nullable<int> bVal = new Nullable<int>(62);

В этом же можно убедиться, опять взглянув на IL код:

// int? aVal;// Nullable<int> bVal;.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,              valuetype [System.Runtime]System.Nullable`1<int32> V_1)// aVal = 62ldloca.s   V_0ldc.i4.s   62call       instance void valuetype                                      [System.Runtime]System.Nullable`1<int32>::.ctor(!0)// bVal = new Nullable<int>(62)ldloca.s   V_1ldc.i4.s   62call       instance void valuetype                                        [System.Runtime]System.Nullable`1<int32>::.ctor(!0)

А что же касается проверок? Например, что на самом деле представляет из себя код следующего вида?

bool IsDefault(int? value) => value == null;

Правильно, для понимания вновь обратимся к соответствующему IL коду.

.method private hidebysig instance boolIsDefault(valuetype [System.Runtime]System.Nullable`1<int32> 'value')cil managed{  .maxstack  8  ldarga.s   'value'  call       instance bool valuetype              [System.Runtime]System.Nullable`1<int32>::get_HasValue()  ldc.i4.0  ceq  ret}

Как вы уже догадались, никакого null на самом деле нет всё, что происходит, это обращение к свойству Nullable<T>.HasValue. То есть, ту же логику в C# можно написать более явно с точки зрения используемых сущностей следующим образом.

bool IsDefaultVerbose(Nullable<int> value) => value.HasValue;

IL код:

.method private hidebysig instance bool IsDefaultVerbose(valuetype [System.Runtime]System.Nullable`1<int32> 'value')cil managed{  .maxstack  8  ldarga.s   'value'  call       instance bool valuetype             [System.Runtime]System.Nullable`1<int32>::get_HasValue()  ret}

Кстати, обратите внимание, что при явной записи компилятор сгенерировал меньше IL кода.

Подытожим:

  • Nullable value types реализуются за счёт типа Nullable<T>;
  • int? на самом деле сконструированный тип обобщённого значимого типа Nullable<T>;
  • int? a = null инициализация объекта типа Nullable<int> значением по умолчанию, никакого null на самом деле здесь нет;
  • if (a == null) опять же, никакого null нет, есть обращение к свойству Nullable<T>.HasValue.

Исходный код типа Nullable<T> можно посмотреть, например, на GitHub в репозитории dotnet/runtime прямая ссылка на файл с исходным кодом. Кода там немного, так что ради интереса советую полистать. Оттуда же можно узнать (или вспомнить) следующие факты.

Для удобства работы тип Nullable<T> определяет:

  • оператор неявного преобразования из T в Nullable<T>;
  • оператор явного преобразования из Nullable<T> в T.

Основная логика работы реализуется за счёт двух полей (и соответствующих свойств):

  • T value само значение, обёрткой над которым является Nullable<T>;
  • bool hasValue флаг, указывающий, "содержит ли обёртка значение". В кавычках, так как по факту Nullable<T> всегда содержит значение типа T.

Теперь, когда мы освежили память по поводу nullable value types, посмотрим, что же там с упаковкой.

Упаковка Nullable<T>


Напомню, что при упаковке объекта значимого типа в куче будет создан новый объект. Это поведение наглядно иллюстрирует следующий фрагмент кода:

int aVal = 62;object obj1 = aVal;object obj2 = aVal;Console.WriteLine(Object.ReferenceEquals(obj1, obj2));

Результатом сравнения ссылок ожидаемо будет false, так как произошло 2 операции упаковки и создание двух объектов, ссылки на которые были записаны в obj1 и obj2.

Теперь меняем int на Nullable<int>.

Nullable<int> aVal = 62;object obj1 = aVal;object obj2 = aVal;Console.WriteLine(Object.ReferenceEquals(obj1, obj2));

Результат всё также ожидаем false.

А теперь вместо 62 прописываем дефолтное значение.

Nullable<int> aVal = new Nullable<int>();object obj1 = aVal;object obj2 = aVal;Console.WriteLine(Object.ReferenceEquals(obj1, obj2));

Иии результатом неожиданно становится true. Казалось бы, имеем всё те же 2 операции упаковки, создание двух объектов и ссылки на два разных объекта, но результат-то true!

Ага, наверняка опять дело в сахаре, и что-то поменялось на уровне IL кода! Давайте посмотрим.

Пример N1.

C# код:

int aVal = 62;object aObj = aVal;

IL код:

.locals init (int32 V_0,              object V_1)// aVal = 62ldc.i4.s   62stloc.0// упаковка aValldloc.0box        [System.Runtime]System.Int32// сохранение полученной ссылки в aObjstloc.1

Пример N2.

C# код:

Nullable<int> aVal = 62;object aObj = aVal;

IL код:

.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,              object V_1)// aVal = new Nullablt<int>(62)ldloca.s   V_0ldc.i4.s   62call       instance void           valuetype [System.Runtime]System.Nullable`1<int32>::.ctor(!0)// упаковка aValldloc.0box        valuetype [System.Runtime]System.Nullable`1<int32>// сохранение полученной ссылки в aObjstloc.1

Пример N3.

C# код:

Nullable<int> aVal = new Nullable<int>();object aObj = aVal;

IL код:

.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,              object V_1)// aVal = new Nullable<int>()ldloca.s   V_0initobj    valuetype [System.Runtime]System.Nullable`1<int32>// упаковка aValldloc.0box        valuetype [System.Runtime]System.Nullable`1<int32>// сохранение полученной ссылки в aObjstloc.1

Как мы видим, везде упаковка происходит идентичным образом значения локальных переменных загружается на evaluation stack (инструкция ldloc), после чего происходит сама упаковка за счёт вызова команды box, для которой указывается, какой, собственно, тип будем упаковывать.

Обращаемся к спецификации Common Language Infrastructure, смотрим описание команды box и находим интересное примечание касаемо nullable типов:

If typeTok is a value type, the box instruction converts val to its boxed form. If it is a nullable type, this is done by inspecting val's HasValue property; if it is false, a null reference is pushed onto the stack; otherwise, the result of boxing val's Value property is pushed onto the stack.

Отсюда следует несколько выводов, расставляющих точки над 'i':

  • учитывается состояние объекта Nullable<T> (проверяется рассмотренный нами ранее флаг HasValue). Если Nullable<T> не содержит значения (HasValue false), результатом упаковки будет null;
  • если Nullable<T> содержит значение (HasValue true), то упакован будет не объект Nullable<T>, а экземпляр типа T, который хранится в поле value типа Nullable<T>;
  • специфичная логика обработки упаковки Nullable<T> реализована не на уровне C# и даже не на уровне IL она реализована в CLR.

Возвращаемся к примерам с Nullable<T>, которые рассматривали выше.

Первый:

Nullable<int> aVal = 62;object obj1 = aVal;object obj2 = aVal;Console.WriteLine(Object.ReferenceEquals(obj1, obj2));

Состояние экземпляра перед упаковкой:

  • T -> int;
  • value -> 62;
  • hasValue -> true.

Два раза происходит упаковка значения 62 (помним, что в данном случае пакуются экземпляры типа int, а не Nullable<int>), создаются 2 новых объекта, получаются 2 ссылки на разные объекты, результат сравнения которых, false.

Второй:

Nullable<int> aVal = new Nullable<int>();object obj1 = aVal;object obj2 = aVal;Console.WriteLine(Object.ReferenceEquals(obj1, obj2));

Состояние экземпляра перед упаковкой:

  • T -> int;
  • value -> default (в данном случае, 0 значение по умолчанию для int);
  • hasValue -> false.

Так как hasValue имеет значение false, не происходит создания объектов в куче, а операция упаковки возвращает значение null, которое и записывается в переменные obj1 и obj2. Сравнение этих значений ожидаемо даёт true.

В исходном примере, который был в самом начале статьи, происходит точно то же самое:

static void NullableTest(){  int? a = null;       // default value of Nullable<int>  object aObj = a;     // null  int? b = new int?(); // default value of Nullable<int>  object bObj = b;     // null  Console.WriteLine(Object.ReferenceEquals(aObj, bObj)); // null == null}

Ради интереса заглянем в исходный код CoreCLR из упомянутого ранее репозитория dotnet/runtime. Нас интересует файл object.cpp, конкретно метод Nullable::Box, который и содержит нужную нам логику:

OBJECTREF Nullable::Box(void* srcPtr, MethodTable* nullableMT){  CONTRACTL  {    THROWS;    GC_TRIGGERS;    MODE_COOPERATIVE;  }  CONTRACTL_END;  FAULT_NOT_FATAL();      // FIX_NOW: why do we need this?  Nullable* src = (Nullable*) srcPtr;  _ASSERTE(IsNullableType(nullableMT));  // We better have a concrete instantiation,   // or our field offset asserts are not useful  _ASSERTE(!nullableMT->ContainsGenericVariables());  if (!*src->HasValueAddr(nullableMT))    return NULL;  OBJECTREF obj = 0;  GCPROTECT_BEGININTERIOR (src);  MethodTable* argMT = nullableMT->GetInstantiation()[0].AsMethodTable();  obj = argMT->Allocate();  CopyValueClass(obj->UnBox(), src->ValueAddr(nullableMT), argMT);  GCPROTECT_END ();  return obj;}

Здесь всё то, о чём мы говорили выше. Если не храним значение возвращаем NULL:

if (!*src->HasValueAddr(nullableMT))    return NULL;

Иначе производим упаковку:

OBJECTREF obj = 0;GCPROTECT_BEGININTERIOR (src);MethodTable* argMT = nullableMT->GetInstantiation()[0].AsMethodTable();obj = argMT->Allocate();CopyValueClass(obj->UnBox(), src->ValueAddr(nullableMT), argMT);

Заключение


Ради интереса предлагаю показать пример из начала статьи своим коллегам и друзьям. Смогут ли они дать верный ответ и обосновать его? Если нет, приглашайте их познакомиться со статьёй. Если же смогут что ж, моё уважение!

Надеюсь, это было небольшое, но увлекательное приключение. :)

P.S. У кого-то мог возникнуть вопрос: а с чего вообще началось погружение в эту тему? Мы делали новое диагностическое правило в PVS-Studio на тему того, что Object.ReferenceEquals работает с аргументами, один из которых представлен значимым типом. Вдруг оказалось, что с Nullable<T> есть неожиданный момент в поведении при упаковке. Посмотрели IL код box как box. Посмотрели спецификацию CLI ага, вот оно! Показалось, что это достаточно интересный кейс, про который стоит рассказать раз! и статья перед вами.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Sergey Vasiliev. Check how you remember nullable value types. Let's peek under the hood.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru