Надежное программирование в разрезе языков. Часть 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 {EXCLUSIVE}
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, разрабатывается Александреску и получилась весьма хитромудрой, полностью построенной на шаблонах. Ключевое для данной темы, что Фобос неконтролируем в плане расхода памяти.
Самое важное что теряется в режиме BetterC — многопоточность, GC, строки, классы (структуры остаются — они близки по функционалу — только на стеке) и исключения (RAII и try-finally остаются).
Возможно, впрочем, часть программы писать на полном D, а критичную часть — на D -BetterC. Также есть системные атрибута функция для контроля неиспользования опасных эффектов: pure safe @nogc.
Обоснование режима от создателя языка.
А тут выжимка — что обрезано, а что осталось доступным.
Строки содержатся в Фобосе — и попытки их использовать в BetterC выливаются в адские ошибки инстантациации шаблонов на элементарных операциях вроде вывода строки на консоль или конкатенации. А в полном режиме D строки в куче и иммутабельные, потому операции с ними приводят к замусориванию памяти.
Мне приходилось несколько раз встречать жалобы на баги в компиляторе. Что впрочем неудивительно для языка, конкурирующего по сложности с С++. При подготовке статьи тоже пришлось столкнуться с 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]);
}
}
Напоследок — сводная таблица соответствия функциональным требованиям.
Наверняка я что то упустил или переврал — так что поправляйте.
Исходники из статьи на гитхабе