Хабрахабр

Надежное программирование в разрезе языков. Часть 2 — Претенденты

Первая часть с функциональными требованиями тут

Заявленные как языки программирования с прицелом на надежность.

В алфавитном порядке — Active Oberon, Ada, BetterC, IEC 61131-3 ST, Safe-C.
Сразу дисклеймер (отмазка) — это никак не агитация “все на левый борт”, и обзор скорее академический — у языка может не быть не только активно поддерживаемой современной среды разработки, но и даже компилятора под Вашу платформу.

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

Реализация есть в учебниках по языку и на форумах, что облегчило мне работу — осталось только адаптировать. Как показатель наглядности языка, я выбрал реализацию известной многопоточной задачи Дейкстры об обедающих философах. Например недавняя хабра статья про современный С++ содержит реализацию на C++17 для сравнения.

Active Oberon (2004)

Создавался с оглядкой на опыт Паскаля, Модулы, предыдущих Оберонов с 1988г, Java, C#, Ады, а также практический опыт применения. Имеет реализацию в виде ОС A2
, которая может выступать рантаймом поверх *nix или Windows. Исходники А2 и компилятора по ссылке

Это немного другой диалект, отличия описаны ниже. Также есть проект Oberon2 to C Compiler (OOC) не привязанный к среде Оберон.

Это 16 страниц по базовому Оберону-2 плюс 23 страницы по многопоточному Активному расширению. Ключевая фишка Оберона — исключительная краткость спецификации.

Простой и понятный синтаксис, исключающий явные ошибки.
Идентификаторы регистрозависимые.
ООП с объектами на куче с автосборщиком мусора(GC).
Отличается от предшественников более привычным синтаксисом ООП в виде Экземпляр.Метод (раньше было Метод(Экземпляр)) и поддержкой многопоточности с примитивами синхронизации.
В реализации ООП нет динамической диспетчеризации, что легко может привести к ситуации — забыли дописать обработку для нового типа.
Потокам можно назначить приоритет и высоким/риалтайм они не прерываются GC.
Строки в виде массивов UTF-8.
Рантайм (Система Оберон) дает интересные возможности для перезапуска сбойной процедуры/модуля/потока в случае рантайм ошибки — адресации памяти или, например, целочисленного переполнения.

Недостатком можно счесть отсутствие RAII, и удобной обработки ошибок — все через коды возврата, за исключением варианта ниже.

Оберон-2 OOC

Удобнее для экспериментов, поскольку не требует ОС Оберон — компилируется в ANSI С и нет проблем интероперабельности. Отличия от Активной версии — нет встроенной в язык многопоточности — вместо этого есть модуль работы с PThreads, зато есть UTF16, иерархическая модульность и системный модуль для работы с исключениями.

Модула-3

Есть еще родственник из немного другой ветки развития в виде Модулы-3. Создавалась на базе Оберона в противовес переусложенной Аде. Реализация тут

Упрощена интеграция с С. По сравнению с Активным Обероном добавлены дженерики и исключения, есть библиотеки для практической работы с Юникодом, GUI, и даже Постгрессом. RAII в виде WITH (похоже на using в C#).
Но похоже, что развитие Модулы-3 остановилось в 2010 году. Другая семантика многопоточности.

Запустив WinAOS я столкнулся с TRAPами (aka abort/stacktrace или runtime error) на ровном месте — даже диспетчер задач работает с ошибками, и хотя система/рантайм и не вылетали — а только приложение, меня посетило определенное сомнение о том, что надежность определяется языком программирования =(
Также AOC является в достаточной степени замкнутой на себя, со своим подходом к разработке. Дисклеймер.

Исходник Обедающих Философов

MODULE Philo;
(* Dining Philosophers Example from Active Oberon Language Report by Patrik Reali *)
(* Adapted for running in AOS by Siemargl *) IMPORT Semaphores := Example8, Out; CONST NofPhilo = 5; (* number of philosophers *) VAR fork: ARRAY NofPhilo OF Semaphores.Semaphore; i: LONGINT; TYPE Philosopher = OBJECT VAR first, second: LONGINT; (* forks used by this philosopher *) PROCEDURE & Init(id: LONGINT); BEGIN IF id # NofPhilo-1 THEN first := id; second := (id+1) ELSE first := 0; second := NofPhilo-1 END END Init; PROCEDURE Think; (* Need lock console output *) BEGIN Out.Int(first); Out.String(".... Think...."); Out.Ln; END Think; PROCEDURE Eat; BEGIN {EXCLUSIVE} Out.Int(first); Out.String(".... Eat...."); Out.Ln; END Eat; BEGIN {ACTIVE} LOOP Think; fork[first].P; fork[second].P; Eat; fork[first].V; fork[second].V END END Philosopher; VAR philo: ARRAY NofPhilo OF Philosopher; BEGIN FOR i := 0 TO NofPhilo DO NEW(fork[i], INTEGER(i)); NEW(philo[i], i); END;
END Philo. Philo.Philo1 ~

Ada (1980, последний действующий стандарт 2016)

Собственно, на первый взгляд тут есть все, что мне хотелось бы.
И даже чуть больше — есть числа с точным вычислениями с плавающей точкой. Например, есть риалтайм планировщик потоков, межпоточный обмен и формально верифицируемое подмножество языка SPARK. И еще много много всего.
Думаю, если бы для надежности Ады был нужен еще черт рогатый, он бы прилагался вместе с инструкцией по вызову в трудной ситуации =)
Реализация — ГНУтая Ада, развивается, стандартизована ISO/IEC.

Требуется ручное управление памятью — и тут возможны ошибки программиста. Стандартом предусмотрена реализация с GC, но для компилируемых вариантов он чаще не реализован. Можно еще определить свою реализацию GC, автоосвобождения или подсчет ссылок для каждого типа данных. Впрочем, язык заточен на использование по умолчанию стека и есть понятие управляемых типов с деструкторами.

Из-за специфичности языковой модели безопасности, интеграция с “чужими” библиотеками затруднена. Ada Reference Manual 2012 содержит 950 страниц.
Недостаток Ады кроме сложности — чрезмерная многословность, что впрочем было задумано в угоду читаемости.

На сайде Ada-ru есть хорошая обзорная переводная статья — первая ссылка.

Исходник Обедающих Философов

-- Code from https://rosettacode.org/wiki/Dining_philosophers#Ordered_mutexes
-- ADA95 compatible so can run in ideone.com with Ada.Numerics.Float_Random; use Ada.Numerics.Float_Random;
with Ada.Text_IO; use Ada.Text_IO; procedure Test_Dining_Philosophers is type Philosopher is (Aristotle, Kant, Spinoza, Marx, Russel); protected type Fork is entry Grab; procedure Put_Down; private Seized : Boolean := False; end Fork; protected body Fork is entry Grab when not Seized is begin Seized := True; end Grab; procedure Put_Down is begin Seized := False; end Put_Down; end Fork; Life_Span : constant := 20; -- In his life a philosopher eats 20 times task type Person (ID : Philosopher; First, Second : not null access Fork); task body Person is Dice : Generator; begin Reset (Dice); for Life_Cycle in 1..Life_Span loop Put_Line (Philosopher'Image (ID) & " is thinking"); delay Duration (Random (Dice) * 0.100); Put_Line (Philosopher'Image (ID) & " is hungry"); First.Grab; Second.Grab; Put_Line (Philosopher'Image (ID) & " is eating"); delay Duration (Random (Dice) * 0.100); Second.Put_Down; First.Put_Down; end loop; Put_Line (Philosopher'Image (ID) & " is leaving"); end Person; Forks : array (1..5) of aliased Fork; -- Forks for hungry philosophers -- Start philosophers Ph_1 : Person (Aristotle, Forks (1)'Access, Forks (2)'Access); Ph_2 : Person (Kant, Forks (2)'Access, Forks (3)'Access); Ph_3 : Person (Spinoza, Forks (3)'Access, Forks (4)'Access); Ph_4 : Person (Marx, Forks (4)'Access, Forks (5)'Access); Ph_5 : Person (Russel, Forks (1)'Access, Forks (5)'Access);
begin null; -- Nothing to do in the main task, just sit and behold
end Test_Dining_Philosophers;

BetterC (dlang subset 2017, оригинальный D — 2001, D 2.0 — 2007)

Самая современная реализация из рассматриваемых. Полное описание языка довольно длинное — 649 страниц — см.оригинальный сайт.
Собственно это язык D, но с ограничениями ключом -betterC. Почему так ?!
Потому что стандартная библиотека D — Phobos, разрабатывается Александреску и получилась весьма хитромудрой, полностью построенной на шаблонах. Ключевое для данной темы, что Фобос неконтролируем в плане расхода памяти.

Также есть системные атрибута функция для контроля неиспользования опасных эффектов: pure safe @nogc.
Обоснование режима от создателя языка.
А тут выжимка — что обрезано, а что осталось доступным. Самое важное что теряется в режиме BetterC — многопоточность, GC, строки, классы (структуры остаются — они близки по функционалу — только на стеке) и исключения (RAII и try-finally остаются).
Возможно, впрочем, часть программы писать на полном D, а критичную часть — на D -BetterC.

А в полном режиме D строки в куче и иммутабельные, потому операции с ними приводят к замусориванию памяти. Строки содержатся в Фобосе — и попытки их использовать в BetterC выливаются в адские ошибки инстантациации шаблонов на элементарных операциях вроде вывода строки на консоль или конкатенации.

Что впрочем неудивительно для языка, конкурирующего по сложности с С++. Мне приходилось несколько раз встречать жалобы на баги в компиляторе. При подготовке статьи тоже пришлось столкнуться с 4мя ошибками — две возникли при попытке собрать dlangide новым компилятором и парой при портировании задачи о философах (например вылет при использовании beginthreadex).

Узнать об этом заранее, какие фичи языка урезаны точно — приходится часто на собственном опыте. Режим еще только недавно появился и ошибки, вызванные ограничением режима BetterC вылезают уже на этапе линковки.

Исходник Обедающих Философов

// compile dmd -betterC import core.sys.windows.windows;
import core.stdc.stdio;
import core.stdc.stdlib : rand;
//import std.typecons; // -impossible (
//import std.string; - impossible extern (Windows) alias btex_fptr = void function(void*) /*nothrow*/;
//extern (C) uintptr_t _beginthreadex(void*, uint, btex_fptr, void*, uint, uint*) nothrow;
/* Dining Philosophers example for a habr.com * by Siemargl, 2019
* BetterC variant. Compile >dmd -betterC Philo_BetterC.d
*/ extern (C) uintptr_t _beginthread(btex_fptr, uint stack_size, void *arglist) nothrow; alias HANDLE uintptr_t;
alias HANDLE Fork; const philocount = 5;
const cycles = 20; HANDLE[philocount] forks; struct Philosopher
{ const(char)* name; Fork left, right; HANDLE lifethread; }
Philosopher[philocount] philos; extern (Windows) void PhilosopherLifeCycle(void* data) nothrow
{ Philosopher* philo = cast(Philosopher*)data; for (int age = 0; age++ < cycles;) { printf("%s is thinking\n", philo.name); Sleep(rand() % 100); printf("%s is hungry\n", philo.name); WaitForSingleObject(philo.left, INFINITE); WaitForSingleObject(philo.right, INFINITE); printf("%s is eating\n", philo.name); Sleep(rand() % 100); ReleaseMutex(philo.right); ReleaseMutex(philo.left); } printf("%s is leaving\n", philo.name);
} extern (C) int main() { version(Windows){} else { static assert(false, "OS not supported"); } philos[0] = Philosopher ("Aristotlet".ptr, forks[0], forks[1], null); philos[1] = Philosopher ("Kant".ptr, forks[1], forks[2], null); philos[2] = Philosopher ("Spinoza".ptr, forks[2], forks[3], null); philos[3] = Philosopher ("Marx".ptr, forks[3], forks[4], null); philos[4] = Philosopher ("Russel".ptr, forks[0], forks[4], null); foreach(ref f; forks) { f = CreateMutex(null, false, null); assert(f); } foreach(ref ph; philos) { ph.lifethread = _beginthread(&PhilosopherLifeCycle, 0, &ph); assert(ph.lifethread); } foreach(ref ph; philos) WaitForSingleObject(ph.lifethread, INFINITE); // Close thread and mutex handles for( auto i = 0; i < philocount; i++ ) { CloseHandle(philos[i].lifethread); CloseHandle(forks[i]); } return 0;
}

Для сравнения, исходник на полном D
На розетте также можно посмотреть варианты для прочих языков.

IEC 61131-3 ST (1993, последний стандарт 2013)

Нишевой язык программирования микроконтроллеров. Стандарт подразумевает 5 вариантов программирования, но писать прикладное приложение к примеру в релейной логике это еще то приключение. Потому сконцентрируемся на одном варианте — структурированный текст.
Текст стандарта ГОСТ Р МЭК 61131-3-2016 — 230 страниц.
Есть реализации для PC/x86 и ARM — и коммерческие, самая известная из которых — это CODESYS (часто еще и сублицензированная с разными именами) и открытые — Beremiz — с трансляцией через С.

С другой стороны — в этой области принято, что логика крутится отдельно и только служит сервером данных для другой программы либо же системы — интерфейса с оператором или с СУБД, которая уже может быть написана уже на чем угодно — без требований реалтайма и даже каких либо временных вообще… Поскольку интеграция с С имеется, то подключить нужные для прикладного программирования библиотеки вполне реально.

Но контроль переполнения зависит от реализации.
В последней редакции стандарта появилось ООП. Многопоточное программирование для пользовательской программы появилось относительно недавно — в микроконтроллерах такое было раньше не нужно.
Приведение типов большей частью только явное (смягчено в последнем стандарте). Это исторически сложилось — количество данных, обрабатываемое микроконтроллером — всегда константно ограничено сверху. Обработка ошибок производится пользовательскими обработчиками прерываний.
Динамического выделения памяти для пользователя можно сказать что нет.

Исходник (не проверялся)

(* Dining Philosophers example for a habr.com * by Siemargl, 2019
* ISO61131 ST language variant. Must be specialized 4 ur PLC * )
CONFIGURATION PLC_1 VAR_GLOBAL Forks : USINT; Philo_1: Philosopher; (* Instance block - static vars *) Philo_2: Philosopher; Philo_3: Philosopher; Philo_4: Philosopher; Philo_5: Philosopher;
END_VAR RESOURCE Station_1 ON CPU_1 TASK Task_1 (INTERVAL := T#100MS, PRIORITY := 1); TASK Task_2 (INTERVAL := T#100MS, PRIORITY := 1); TASK Task_3 (INTERVAL := T#100MS, PRIORITY := 1); TASK Task_4 (INTERVAL := T#100MS, PRIORITY := 1); TASK Task_5 (INTERVAL := T#100MS, PRIORITY := 1); PROGRAM Life_1 WITH Task_1: Philo_1(Name := 'Kant', 0, 1, Forks); PROGRAM Life2 WITH Task_2: Philo_2(Name := 'Aristotel', 1, 2, Forks); PROGRAM Life3 WITH Task_3: Philo_3(Name := 'Spinoza', 2, 3, Forks); PROGRAM Life4 WITH Task_4: Philo_4(Name := 'Marx', 3, 4, Forks); PROGRAM Life5 WITH Task_5: Philo_5(Name := 'Russel', 4, 0, Forks); END_RESOURCE END_CONFIGURATION FUNCTION_BLOCK Philosopher;
USING SysCpuHandling.library;
VAR_INPUT Name: STRING; Left: UINT; Right: UINT;
END_VAR
VAR_IN_OUT Forks: USINT;
END_VAR
VAR Thinking: BOOL := TRUE; (* States *) Hungry: BOOL; Eating: BOOL; HaveLeftFork: BOOL; TmThink: TON; TmEating: TON;
END_VAR TmThink(In := Thinking; PT := T#3s); TmEating(In := Eating; PT := T#5s); IF Thinking THEN (* Just waiting Timer *) Thinking := NOT TmThink.Q; Hungry := TmThink.Q; ELSIF Hungry (* Try Atomic Lock Forks *) IF HaveLeftFork IF SysCpuTestAndSetBit(Address := Forks, Len := 1, iBit := Right, bSet := 1) = ERR_OK THEN Hungry := FALSE; Eating := TRUE; ELSE RETURN; END_IF ELSIF IF SysCpuTestAndSetBit(Address := Forks, Len := 1, iBit := Left, bSet := 1) = ERR_OK THEN HaveLeftFork := TRUE; ELSE RETURN; END_IF END_IF ELSIF Eating (* Waiting Timer, then lay forks *) IF TmEating.Q THEN Thinking := TRUE; Eating := FALSE; HaveLeftFork := FALSE; SysCpuTestAndSetBit(Address := Forks, Len := 1, iBit := Right, bSet := 0); SysCpuTestAndSetBit(Address := Forks, Len := 1, iBit := Left, bSet := 0); END_IF END_IF
END_FUNCTION_BLOCK

Safe-C (2011)

Экспериментальный С с удалением опасных фишек и с добавлением модульности и многопоточности. Сайт проекта
Описание примерно 103 страницы. Если выделить отличия от С — совсем мало, около 10.

Работа с массивами и указателями обезопасена, динамическая память с автоматическим подсчетом ссылок — с проверками на двойное освобождении и повисшие ссылки.

В стандартной библиотеке есть минимальный набор функций для GUI, многопоточности, сетевых функций (в т.ч http-сервер).

Хотя код компилятора и библиотеки открыт. Но — данная реализация только для Windows x86.

В рамках другой исследовательской задачи я собрал макет Веб-сервер, собирающий данные с IoT датчиков: 75 Кб исполнительный модуль, и < 1Мб частичный набор памяти.

Исходник Обедающих Философов

/* Dining Philosophers example for a habr.com * by Siemargl, 2019
* Safe-C variant. Compile >mk.exe philosafec.c
*/ from std use console, thread, random; enum philos (ushort) { Aristotle, Kant, Spinoza, Marx, Russell, };
const int cycles = 10;
const ushort NUM = 5;
uint lived = NUM; packed struct philosopher // 32-bit
{ philos name; byte left, right;
} philosopher philo_body[NUM]; SHARED_OBJECT forks[NUM]; void philosopher_life(philosopher philo) { int age; for (age = 0; age++ < cycles; ) { printf("%s is thinking\n", philo.name'string); delay((uint)rnd(1, 100)); printf("%s is hungry\n", philo.name'string); enter_shared_object(ref forks[philo.left]); enter_shared_object(ref forks[philo.right]); printf("%s is eating\n", philo.name'string); delay((uint)rnd(1, 100)); leave_shared_object(ref forks[philo.right]); leave_shared_object(ref forks[philo.left]); } printf("%s is leaving\n", philo.name'string); InterlockedExchange(ref lived, lived-1);
} void main()
{ philos i; assert philosopher'size == 4; philo_body[0] = {Aristotle, 0, 1}; philo_body[1] = {Kant, 1, 2}; philo_body[2] = {Spinoza, 2, 3}; philo_body[3] = {Marx, 3, 4}; philo_body[4] = {Russell, 0, 4}; for (i = philos'first; i <= philos'last; i++) { assert run philosopher_life(philo_body[(uint)i]) == 0; } while (lived > 0) sleep 0; // until all dies for (i = philos'first; i <= philos'last; i++) { destroy_shared_object(ref forks[(uint)i]); }
}

Напоследок — сводная таблица соответствия функциональным требованиям.
Наверняка я что то упустил или переврал — так что поправляйте.

Исходники из статьи на гитхабе

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»