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

habr.png

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

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

В алфавитном порядке — 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]);
        }
}


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

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

© Habrahabr.ru