Programación confiable en el contexto de los lenguajes. Parte 2 - Retadores

La primera parte con requisitos funcionales está aquí .

Reclamado como lenguajes de programación con un ojo puesto en la fiabilidad.

Alfabéticamente: Active Oberon, Ada, BetterC, IEC 61131-3 ST, Safe-C.

A la vez, el descargo de responsabilidad (excusa) no es en modo alguno una campaña de "todos del lado izquierdo", y la revisión es más bien académica: el lenguaje no solo puede tener un entorno de desarrollo moderno respaldado activamente, sino incluso un compilador para su plataforma.

Por otro lado, para los idiomas en cuestión hay compiladores de código abierto, y con el nivel actual de desarrollo de software, con interés, la sintaxis no demasiado complicada le permite crear un compilador personal e integrarse en algún tipo de Eclipse con luz de fondo y analizador.

Como indicador de la claridad del lenguaje, elegí la implementación de la famosa tarea multihilo de Dijkstra sobre los filósofos de la gastronomía. La implementación se encuentra en los libros de texto sobre el idioma y en los foros, lo que facilitó mi trabajo; solo queda adaptarme. Por ejemplo, un artículo reciente de habr sobre C ++ moderno contiene una implementación en C ++ 17 para comparar.

Oberon activo (2004)


Fue creado teniendo en cuenta la experiencia de Pascal, Modula, Oberons anteriores desde 1988, Java, C #, Ada, así como la experiencia práctica en la aplicación. Tiene una implementación en forma de OS A2 , que puede actuar como tiempo de ejecución sobre * nix o Windows. Fuentes A2 y el compilador para el enlace .

También hay un proyecto de compilador de Oberon2 a C (OOC) que no está vinculado al entorno de Oberon. Este es un dialecto ligeramente diferente, las diferencias se describen a continuación.

La característica clave de Oberon es la brevedad excepcional de la especificación. Estas son 16 páginas en la base Oberon-2 más 23 páginas en la extensión activa de subprocesos múltiples.

Sintaxis simple y clara que excluye errores obvios.

Los identificadores distinguen entre mayúsculas y minúsculas.

OOP con objetos en el montón con el recolector de basura (GC).

Se diferencia de sus predecesores en la sintaxis OOP más familiar en forma de Instance.Method (solía ser Method (Instance)) y soporte para subprocesos múltiples con primitivas de sincronización.
No existe un despacho dinámico en la implementación de OOP, lo que puede conducir fácilmente a una situación: se olvidaron de agregar procesamiento para un nuevo tipo.

Se puede asignar prioridad a las transmisiones y, en tiempo real / alto, GC no las interrumpe. Cadenas en forma de matrices UTF-8.

El Rantime (Sistema Oberon) ofrece oportunidades interesantes para reiniciar un procedimiento / módulo / subproceso fallido en caso de un error de tiempo de ejecución: direccionamiento de memoria o, por ejemplo, desbordamiento de enteros.

La desventaja es la falta de RAII y el manejo conveniente de errores, todo a través de códigos de retorno, con la excepción de la opción a continuación.

Oberon-2 OOC


Es más conveniente para los experimentos, ya que Oberon no requiere sistema operativo: se compila en ANSI C y no hay problemas de interoperabilidad. Diferencias con la versión activa: no hay un lenguaje de subprocesamiento múltiple incorporado; en cambio, hay un módulo para trabajar con PThreads, pero hay UTF16, modularidad jerárquica y un módulo de sistema para trabajar con excepciones.

Módulo 3


También hay un pariente de una rama de desarrollo ligeramente diferente en forma de Modula-3. Fue creado sobre la base de Oberon en oposición a la sobredesarrollada Ada. La implementación está aquí .

En comparación con Active Oberon, se agregan genéricos y excepciones, hay bibliotecas para el trabajo práctico con Unicode, GUI e incluso Postgress. Integración simplificada con C. Otra semántica multihilo. RAII como WITH (similar al uso en C #).

Pero parece que el desarrollo de Modula 3 se detuvo en 2010.

Descargo de responsabilidad. Después de lanzar WinAOS, me encontré con TRAPs (también conocido como abortar / stacktrace o error de tiempo de ejecución) de la nada, incluso el administrador de tareas no funciona correctamente, y aunque el sistema / tiempo de ejecución no se bloqueó, pero solo la aplicación, tuve una cierta duda de que la fiabilidad está determinada por el idioma programación = (

Además, AOC es suficientemente autónomo, con su enfoque de desarrollo.

Fuente para los filósofos gastronómicos
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, último estándar válido de 2016)


En realidad, a primera vista hay todo lo que me gustaría.

E incluso un poco más: hay números con cálculos exactos de coma flotante. Por ejemplo, hay un planificador de subprocesos en tiempo real, intercambio de subprocesos cruzados y un subconjunto verificado formalmente del lenguaje SPARK. Y mucho mas.

Creo que si la confiabilidad de Ada necesitara una maldita con cuernos, se adjuntaría con instrucciones para llamar en una situación difícil =)

Implementación - GNUTaya Ada , está en desarrollo, ISO / IEC estandarizado.

El estándar proporciona implementación con GC, pero para las opciones compiladas a menudo no se implementa. Se requiere la gestión manual de la memoria, y aquí son posibles los errores del programador. Sin embargo, el lenguaje está orientado al uso de la pila predeterminada y existe el concepto de tipos administrados con destructores. También puede definir su implementación de GC, liberación automática o recuento de referencias para cada tipo de datos.

Ada Reference Manual 2012 contiene 950 páginas.

La desventaja de Ada, además de la complejidad, es su excesiva verbosidad, que, sin embargo, fue concebida en aras de la legibilidad. Debido a la especificidad del modelo de seguridad del idioma, la integración con bibliotecas extranjeras es difícil.

El sitio Ada-ru tiene un buen artículo de traducción de revisión: el primer enlace.

Fuente para los filósofos gastronómicos
 -- 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 (subconjunto de dlang 2017, D original - 2001, D 2.0 - 2007)


La implementación más moderna de lo considerado. La descripción completa del idioma es bastante larga - 649 páginas - vea el sitio original .

En realidad, este es el lenguaje D, pero con restricciones con el modificador -betterC. ¿Por qué así?

Porque la biblioteca estándar D es Phobos, desarrollada por Alexandrescu y resultó ser muy astuta, completamente construida en plantillas. La clave de este tema es que Phobos es incontrolable en términos de consumo de memoria.

Las cosas más importantes que se pierden en el modo BetterC son multihilo, GC, cadenas, clases (las estructuras permanecen, tienen una funcionalidad cercana, solo en la pila) y las excepciones (RAII y try-finally permanecen).

Sin embargo, es posible escribir parte del programa en D completa y la parte crítica en D-BetterC. También hay una función de atributo del sistema para controlar la no utilización de efectos peligrosos: pure safe @nogc.

Justificación del régimen del creador del lenguaje.

Y luego el apretón : qué se corta y qué queda disponible.

Las cadenas están contenidas en Phobos, y los intentos de usarlas en BetterC resultan en errores infernales de creación de instancias de plantillas en operaciones elementales como la salida de una cadena a la consola o concatenación. Y en modo D completo, las líneas en el montón también son inmutables, por lo tanto, las operaciones con ellas conducen al desorden de memoria.

Tuve que encontrar quejas sobre errores en el compilador varias veces. Lo cual, sin embargo, no es sorprendente para un lenguaje que compite en complejidad con C ++. Al preparar el artículo, también tuve que enfrentar 4 errores: dos surgieron al intentar construir dlangide con un nuevo compilador y un par al portar el problema del filósofo (por ejemplo, bloqueo al usar beginthreadex).

El modo ha aparecido recientemente y los errores causados ​​por la restricción del modo BetterC ya se encuentran en la etapa de vinculación. Para aprender sobre esto de antemano, qué características del lenguaje se recortan exactamente, a menudo tienen que hacerlo de primera mano.

Fuente para los filósofos gastronómicos
 // 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; } 


A modo de comparación, la fuente está llena D.

En la roseta también puede ver opciones para otros idiomas.

IEC 61131-3 ST (1993, último estándar 2013)


Un lenguaje de programación de nicho para microcontroladores. El estándar implica 5 opciones de programación, pero escribir una aplicación, por ejemplo, en lógica de escalera sigue siendo una aventura. Por lo tanto, nos centramos en una opción: el texto estructurado.
El texto de la norma GOST R IEC 61131-3-2016 - 230 páginas.

Hay implementaciones para PC / x86 y ARM, y comerciales, la más famosa de las cuales es CODESYS (a menudo también con licencia con diferentes nombres) y abierta, Beremiz , transmitida a través de C.

Como hay integración con C, es bastante posible conectar las bibliotecas necesarias para la programación aplicada. Por otro lado, en esta área se acepta que la lógica gira por separado y solo sirve como un servidor de datos para otro programa o sistema, una interfaz con un operador o con un DBMS que ya se puede escribir en cualquier cosa, sin requisitos en tiempo real o incluso temporal. en general ...

La programación multiproceso para un programa de usuario ha aparecido relativamente recientemente; en microcontroladores esto no era necesario antes.

La conversión de tipos es principalmente explícita (relajada en el último estándar). Pero el control de desbordamiento depende de la implementación.

En la última edición del estándar, apareció OOP. El manejo de errores se realiza mediante manejadores de interrupciones personalizados.

Podemos decir que no hay asignación de memoria dinámica para el usuario. Esto sucedió históricamente: la cantidad de datos procesados ​​por el microcontrolador siempre está constantemente limitada desde arriba.

Fuente (no verificado)
 (* 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)


Experimental C con la eliminación de chips peligrosos y con la adición de modularidad y multihilo. Sitio del proyecto
Descripción de aproximadamente 103 páginas. Si resalta las diferencias de C, muy poco, alrededor de 10 .

Trabajar con matrices y punteros es una memoria segura y dinámica con conteo automático de referencias, con comprobaciones de doble liberación y enlaces colgantes.

La biblioteca estándar tiene un conjunto mínimo de funciones para la GUI, multihilo, funciones de red (incluido un servidor http).

Pero, esta implementación es solo para Windows x86. Aunque el código del compilador y la biblioteca está abierto.

Como parte de otra tarea de investigación, armé un diseño de servidor web que recopila datos de sensores IoT: un módulo ejecutivo de 75 Kb y un conjunto de memoria parcial de <1 MB.

Fuente para los filósofos gastronómicos
 /* 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]); } } 


Finalmente, una tabla resumen de cumplimiento de los requisitos funcionales.
Seguramente me perdí o malinterpreté algo, así que corríjalo.

Fuentes del artículo sobre github .

Source: https://habr.com/ru/post/442372/


All Articles