Стартап Trasec разрабатывает новый язык программирования, который называется TrapC. Авторы проекта провозглашают его «наследником» C и C++. ЯП обещает устранить главные проблемы «предков», включая небезопасное управление памятью. Для этого в TrapC внедрены автоматические проверки и защита программ от типичных ошибок. Это делает невозможным выход за границы буфера или обращение к несуществующей памяти и значительно усложняет жизнь хакерам. Давайте оценим новинку.

В чем проблема C и C++

C и C++ — популярные языки программирования, «пионеры» отрасли, одна из основ современной ИТ-индустрии. Но у них есть проблемы. Они предоставляют разработчикам прямой доступ к управлению памятью, что, с одной стороны, обеспечивает высокую производительность и гибкость. С другой — эта же фича делает программы уязвимыми к ошибкам, связанным с памятью. Вот основные проблемы, ставшие поводом для критики:

  • переполнение буфера: классическая уязвимость, которую хакеры используют для выполнения вредоносного кода;

  • ошибки с указателями: некорректное использование ведет к обращению к несуществующей или освобожденной памяти;

  • утечки памяти: из-за ошибок в коде разработчики часто не высвобождают память, что приводит к росту потребления ресурсов;

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

Все это может привести к непредсказуемому поведению программ и создать лазейки для хакерских атак. Из-за этих проблем многие компании и отдельные разработчики начали искать альтернативы. Rust — один из главных претендентов, но переход на него связан с большими затратами времени и ресурсов.

TrapC появляется в качестве «философского камня», решающего все проблемы. Он предлагает концепцию «минимальной боли» благодаря своей совместимости с кодом упомянутых языков программирования.

TrapC разрабатывается стартапом Trasec, основанным Робином Роу и Габриэль Пантерой. Робин Роу — бывший профессор компьютерных наук, участвовавший в комитетах по развитию стандартов C и C++. Он также известен как соавтор графического редактора Cinepaint, использовавшегося при создании графики для ряда голливудских фильмов. Габриэль Пантера ранее занимала руководящую должность в компании Disney.

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

TrapC: основные достоинства

У диалекта немало плюсов по сравнению с «обычными» языками программирования. Посмотрим, о чем нам рассказывают авторы проекта.

Революция в управлении памятью

Одно из центральных нововведений TrapC — переработанный подход к управлению указателями. Система обработчиков исключений (trap) следит за их безопасностью и предотвращает:

  • выход за границы буфера;

  • обращение к освобожденной памяти;

  • попытки некорректного преобразования типов.

Механизм интегрирован на уровне компилятора, что исключает необходимость в ручном управлении этими аспектами и существенно снижает вероятность ошибок. Вот пример кода. Заявлено, что TrapC не даст переполнить буфер buff при выполнении "strcpy(buff,argv[1]);" или не даст увеличить указатель или индекс массива на значение, смещающее его за пределы выделенного буфера или конца массива:

// darpa_tractor.c
   int main(int argc,char* argv[])
   {   
           char buff[8]; // TrapC implicitly zeros, no dirty memory    
           int success = 0;// In C, buffer overwrite corrupts success    
           strcpy(buff,argv[1]); // TrapC cannot overrun, strcpy safe    
           if(!strcmp(buff,"s3cr8tpw"))
           {       
                success = 1;    
           }    
           if(success) // TrapC blocked strcpy overwrite, success good    
           {       
                printf("Welcome!\n");    
           }      
           return !success;
   }

   // trapc_ptr.c

   int main()
   {   
           const char* ptr = "Hello World"; // 12 char wide    
           while(ptr) // No buffer overrun with TrapC 
           {       
                printf("%c",*ptr); // print one char at a time 
                ptr++;  // Steps off the end: TrapC nulls ptr!
           }  // Do NOT attempt this in C, will segfault!
           assert(ptr == 0);    
           return 0;
   }

   // trapc_array.c

   int score[10]; 
   printf("%i",score[-1]); // TrapC will not allow access
   for(int i = 0;i <= INT_MAX;i++) // C Undefined Behavior
   {  
        printf("%i",i);
   }
   // In C, above code is an infinite loop, will not stop.  
   // TrapC blocks i++ overflow wrap-around, stops.
   // TrapC will fail-safe, call error handler if defined.

Еще одна полезная фича — автоматическое управление памятью. В отличие от C и C++, где программист отвечает за выделение и освобождение памяти через вызовы malloc, free, new или delete, TrapC берет эту задачу на себя. Новый компилятор:

  • автоматически отслеживает, где память больше не используется;

  • высвобождает ее, предотвращая утечки;

  • анализирует код на этапе компиляции, чтобы указать на потенциальные проблемы.

Такой подход чем-то похож на принципы работы «сборщиков мусора», используемых в языках Java и Python. Но он сохраняет высокую производительность благодаря оптимизации работы компилятора.

Упрощение синтаксиса

TrapC устраняет избыточные ключевые слова и структуры. Например:

  • удалено слово union, которое в C и C++ использовалось для определения объединений, но часто приводило к ошибкам;

  • исключены сложные конструкции для ручного управления памятью;

  • отсутствуют макросы: упрощение синтаксиса и устранение ошибок, связанных с их использованием;

  • простое преобразование типов: меньше ошибок, связанных с некорректным приведением типов.

«У TrapC меньше ключевых слов, чем у C, потому что я убрал union и другие вещи. В первую очередь те, что я редко использую, и которые, как правило, вызывают проблемы. Да и в системах, критически важных для безопасности, вы их вряд ли будете использовать», — заявил Роу. По его словам, это делает TrapC более доступным для начинающих программистов, снижая порог вхождения, при этом опытные разработчики ценят гибкость и совместимость с привычными инструментами.

Безопасность без потери производительности

Многие опасаются, что добавление дополнительных проверок и автоматического управления памятью снизит производительность кода. Но разработчики TrapC уверяют, что их компилятор минимизирует накладные расходы, выполняя большинство проверок во время компиляции, а не работы программы. Это позволяет TrapC сохранять конкурентоспособность по скорости с C и C++, оставаясь более безопасным.

Будущее TrapC

Стартап Trasec планирует открыть исходный код компилятора TrapC в 2025 году — это станет важным шагом в плане популяризации языка. Open-source-код даст возможность сообществу разработчиков:

  • вносить улучшения в компилятор;

  • адаптировать язык под специфические задачи;

  • создавать собственные инструменты и библиотеки для работы с TrapC.

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

TrapC уже вызывает интерес у компаний, работающих с критически важным ПО, где каждая ошибка может стоить миллионов. Основные отрасли, где может пригодиться этот диалект:

  • ПО для авиации и медицины;

  • создание банковских и финансовых систем;

  • разработка ядра операционных систем.

Кстати, можно сравнить возможности новинки с Rust. Последний считается одной из самых популярных альтернатив C и C++, но TrapC предлагает иной подход. Если Rust полностью пересматривает концепцию работы с памятью, делая язык сложным для освоения, TrapC старается сохранить простоту и привычность C, убрав при этом его недостатки.

Эти два языка могут сосуществовать, предлагая разработчикам выбор в зависимости от их целей и навыков. У TrapC есть все шансы занять свою нишу среди языков программирования, активно используемых в критически важных областях. Но станет ли он популярным? Как говорят, поживем — увидим.

Комментарии (41)


  1. hardtop
    04.12.2024 07:33

    Так ещё можно zig посмотреть. Он, правда молодой и меняется, зато компилирует Си сам из коробки.


  1. Vad344
    04.12.2024 07:33

    Ну какой же это "наследник С++"?

    И рядом не стояло.


  1. Ydav359
    04.12.2024 07:33

    Это же кошмар: меньше ключевых слов, чем в C, не понятно, завезли ли bool, строки и классы, еще и макросы убрали (а как же сишный #define true 1)


  1. Gay_Lussak
    04.12.2024 07:33

    Пока видно, что лучше некоторые моменты только по сравнению с С. Потому что аргументы против С++ просто смешны. Никто в здравом уме в продуктовом коде не будет использовать new, buf[], union и прочий С++89. Хочешь безопасно управлять ресурсами - вот тебе смарт поинтеры, хочешь контролировать выход за границы - вот тебе вектор с исключениями, хочешь структуру, хранящую разные типы - вот тебе variant. И внезапно все проблемы, описанные в статье исчезают.


    1. eao197
      04.12.2024 07:33

      Никто в здравом уме в продуктовом коде не будет использовать new, buf[], union и прочий С++89.

      Как ни странно, но для union-а до сих пор есть место в продуктовом коде. Поскольку union применяют для экономии места и когда есть возможность определить тип содержимого union-а внешними средствами (или через общий префикс для содержащихся в union-е структур), то union-ом можно сэкономить, по меньшей мере, байт на каждый экземпляр union-а. А когда таких экземпляров пара-тройка миллионов, то и экономия получается немаленькая. Особенно с учетом эффекта выравнивания данных.


  1. Abstraction
    04.12.2024 07:33

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

    А это как, простите? В этих искусственных примерах - верю, но в них и любой линтер вроде cppcheck ругнётся. А если у меня в функцию передан указатель на начало массива и из совершенно другой переменной выводится его длина, то компилятор же хрен докажет что длина корректная, это нужно анализировать всю программу.

    отсутствуют макросы: упрощение синтаксиса и устранение ошибок, связанных с их использованием;

    Замена того что делалось макросами на примерно эквивалентные им шаблоны упростит синтаксис? Ну-ну.

    Но (код) сохраняет высокую производительность благодаря оптимизации работы компилятора.

    Какой-то словесный салат. Работающий эффективно компилятор и компилятор, порождающий эффективный код - это очевидно разные вещи. "Автоматически освобождает память" - программа/среда исполнения, а не компилятор. Если компилятор вставил в код проверку, а не надо ли "автоматически" освободить память - эта проверка съела минимум одну ассемблерную инструкцию, чудес не будет. (А в многопоточной программе такие проверки на произвольную память делать больно, многопоточные структуры стараются разнести во времени работу с данными и выделение памяти.)


  1. serg_meus
    04.12.2024 07:33

    Ada, Java, C#, D, Rust, Julia -- вот лишь верхушка длинного списка убийц языка C и примкнувшего к нему C++. Убивают-убивают, а воз и ныне там, некоторые убийцы сами канули в Лету.
    Надо просто относиться к C как к мультиплатформенному языку ассемблера, чем он и был замыслен изначально. Есть в ассемблере неопреденное поведение, указатели и ручное управление памятью? Есть, и в этом часть его силы.
    Как по мне, на замену C больше всего годен язык V, но он пока в бета версии.


    1. Ydav359
      04.12.2024 07:33

      Julia скорее про убийство фортрана)


    1. QtRoS
      04.12.2024 07:33

      V скорее про убийство Go)


    1. Vladislav_Dudnikov
      04.12.2024 07:33

      В V же сборщик мусора есть, как он может быть убийцей Си в микроконтроллерах, например? Zig тогда уж лучший кандидат (ну или Rust).


      1. serg_meus
        04.12.2024 07:33

        Сборщик мусора отключается ключом компилятора -gc none


        1. qw1
          04.12.2024 07:33

          Но в языке нет new/malloc, соответственно нет и free. При выключении gc начинает течь память, или как удаляются аллоцированные объекты?


          1. serg_meus
            04.12.2024 07:33

            New и malloc нет. Память выделяется при декларирования (инициализации) объектов оператором := или при добавлении их в коллекции. А вот free есть. У объекта можно вызвать метод free(), обернув вызов в блок unsafe{}, и тогда произойдёт его освобождение. Я проверял под Windows и Linux, память системе возвращается.


            1. qw1
              04.12.2024 07:33

              Получается другой диалект языка. Если в V принято писать с gc, и все библиотеки так написаны, то просто добавление ключа компилятора всё сломает.


              1. serg_meus
                04.12.2024 07:33

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


                1. qw1
                  04.12.2024 07:33

                  unsafe убивает преимущества языка по безопасности, тогда уж можно писать на Си.

                  Можно, конечно, возразить, что критичные места пишем в unsafe, остальное в safe, но получается типичная java, где "всё критичное напишем на C++", а в итоге выходит, что GUI и простыни бизнес-логики написаны на языке с GC, и в целом система тормозит "размазанно", не получается найти одно то место, оптимизация которого даст 80% скорости.


                  1. serg_meus
                    04.12.2024 07:33

                    Полностью согласен. Ждём, когда появится язык максимально быстрый, 100% безопасный, лаконичный, выразительный и без unsafe. Возможно, таковой появится, когда достаточно разовьётся ИИ.


              1. Siemargl
                04.12.2024 07:33

                Не совсем. Но под работу без gc придётся собрать свой стек библиотек.

                В BetterC та же проблема.

                Как модный вариант - бить на микросервисы, часть надёжные, часть как получится =)


    1. ahabreader
      04.12.2024 07:33

      Ada, Java, C#, D, Rust, Julia -- вот лишь верхушка длинного списка убийц языка C и примкнувшего к нему C++.

      В этом списке только Rust целится именно на C и большое будущее ему гарантировано как минимум административными методами: движением правительства США за memory safety. Оно поставило вопрос ребром: не "приёмы безопасной работы с памятью", а "memory-safe языки", не "но в этом коде нет ручной работы с памятью", а "это код на memory-unsafe языке", не "декларация, как сделать мир лучше", а "пошевелитесь, иначе вы скоро заказ не получите". И какие остаются популярные языки кроме Rust в нише языков без сборки мусора?

      Есть в ассемблере неопреденное поведение, указатели и ручное управление памятью? Есть, и в этом часть его силы.

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


      1. geher
        04.12.2024 07:33

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

        Так неопределенное поведение - это не само выкидывание кода, а только повод для него. А суть нетпределенного поведения в том, что предсказать это самое поведение программы невозможно. Например, если разименовать неинициализированный указатель, то можно попасть в нормальгый участок памяти и прочитать непонятно что, а можно вывалиться в недоступную память и вывалиться по попытке доступа к ней. Сишный компилятор замечает это UB и может выкинуть код исходя из соображения, что неопределенного птвпдения быть не может.

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

        По моему как-то так


        1. qw1
          04.12.2024 07:33

          одержимое регистра получается случайным (что осталось от предыдущих операций), а поведение программы становится неопределенным

          Я с этим не согласен. Поведение программы остаётся определённым, в регистре сохраняется результат от предыдущей операции.

          Это примерно как в C++ выкинуть закомментировать строчку ниже и говорить, что появилось UB

              int a = func1();
              if (a == 0) return 0;
              // a = func2();
              if (a == 0) return 0;
          

          Нет тут UB.


      1. serg_meus
        04.12.2024 07:33

        Неопределённого поведения в нём всё-таки нет

        Как это нет? А если я загружу в регистры и буду использовать неинициализированную ячейку памяти, то это что? Или коды введённых с клавиатуры символов буду сохранять в памяти программы, то это что?


        1. kibb
          04.12.2024 07:33

          Исполнятся те инструкции, коды которых записались в память.

          Неициализированной памяти на уровне ISA (у обычных машин) не бывает, каждый байт содержит значение, которое является числом от 0 до 255.

          Можно придумать теговую архитектуру, в которой у каждой ячейки будет дополнительный атрибут 'инициализировано', и которая будет трапаться при доступе к не инициализированной памяти (сигнальному значению). Но поведение все равно останется вполне определенным - сигнал о доступе к неинициализированной памяти.


        1. qw1
          04.12.2024 07:33

          Или коды введённых с клавиатуры символов буду сохранять в памяти программы, то это что?

          И что, в программе ниже есть UB?

          #include <conio.h>
          int main()
          {
              return kbhit();
          }
          


          1. serg_meus
            04.12.2024 07:33

            Я имел в виду вот это:

            #include <stdio.h>
            void main() {
                void *pMainAdr=(void *)&main;
                scanf("%s", pMainAdr);
            }
            


            1. qw1
              04.12.2024 07:33

              Если так, то загрузчик DLL и EXE - тоже UB? Потому что кто там знает, какой код лежит в загружаемых файлах...


              1. serg_meus
                04.12.2024 07:33

                Вызов произвольного кода это синоним неопределённого поведения. Если перед каждой загрузкой подменять DLL или EXE -- то это UB, иначе DB


                1. code07734
                  04.12.2024 07:33

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

                  Если же вы на ассемблере напишете программу которая читает с произвольной памяти или вызывает произвольный код - неизвестно какой будет результат выполнения, но поведение программы задано в явном виде в точности как вы её написали

                  Проблема останова и UB разные вещи


    1. kibb
      04.12.2024 07:33

      В ассемблере как раз UB нет. ISA для машины полностью определяет допустимые результаты исполнения.


      1. qw1
        04.12.2024 07:33

        Строго говоря, в древних ассемблерах типа 6502, Z80 были неопределённые опкоды и регистры, которые не документированы у официального разработчика архитектуры (но были написаны десятки книжек типа "Секреты процессора XXX", и программисты могли их использовать), а производители клонов этих процессоров либо не реализовывали эти опкоды, либо реализовывали их по-другому. Также, часто не документировались все биты флагового регистра. Например, флаги переноса/полупереноса были строго документированы для ADD/SUB, но плавали в логических операциях (AND/OR/XOR) - где-то не менялись, где-то копировались из соответствующей позиции источника или результата.


        1. code07734
          04.12.2024 07:33

          Unspecified behavior


          1. qw1
            04.12.2024 07:33

            Нет, в рамках ISA это именно Undefined. А когда берём конкретный чип, от Zilog, или немецкий клон от VEB, поведение становится Unspecified.
            Аналогично можно сказать, что в C++ нет Undefined: берём конкретную версию конкретного GCC и получаем строго детерминированную компиляцию, пусть и Unspecified.


            1. code07734
              04.12.2024 07:33

              Термин UB означает то что я уже выше написал. Факт того что вам неизвестны инструкции какого-то процессора по какой-любо причине - не называют UB. Явление действительно похожее - в случае UB с конкретным компилятором неопределенность уходит. А тут с конкретной реализацией процессора. Но UB всё таки про компиляцию ЯВУ


  1. orefkov
    04.12.2024 07:33

    C++: "Многие ждали меня в лесу, возле выкопанной ямы, знал бы ты, как им теперь к лицу пиджачок деревянный"


  1. yerm
    04.12.2024 07:33

    Вроде C# в своё время претендовал на роль убийцы С++. Во всяком случае при завершении очередного проекта на С++ в середине нулевых мне предложили либо переходить в проект на С#, либо уволиться.


  1. Devastator82
    04.12.2024 07:33

    Скрытый текст

    Все стартапы обещают нам полеты на Марс за пару баксов. Дождемся релиза и посмотрим в деле не на выдуманных искусственных примерах. Простите за гифку - не удержался)


  1. 9241304
    04.12.2024 07:33

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


  1. Siemargl
    04.12.2024 07:33

    Когда пришла пора ещё раз продать safe-c.org j=)


  1. Nemoumbra
    04.12.2024 07:33

    Я смотрел видео-презентацию этого TrapC, там было больше информации...

    1. Они обещали классы завезти, с конструкторами и деструкторами. Причём слово new всё-таки есть, и означает оно там как раз создание объекта. А вот delete нет.

    2. Гарантии отсутствия сегфолтов, утечек памяти или обращений вне границ. Никаких "garbage in => garbage out".

    3. Скорость - не медленнее C/C++...

    4. ...однако в TrapC нет UB!

    5. GC работает "не как в Java".

    6. Встроенный тип "decimal". А ещё все принтфо-образные и сканфо-образные функции libc умеют работать с "%j" - JSON. В примере они суют какую-то структуру типа Point в printf по указателю, и она выводится в виде JSON.

    7. Язык "простой": в нем нет наследования (да и нужно нам энто ваше наследование), нет шаблонов (не жили хорошо, нечего и начинать), нет перегрузок функций (горит сарай, гори и хата).

    8. Одним словом, убийца C++! /s


  1. Noah1
    04.12.2024 07:33

    Зачем очередная замена C? Уже есть Zig, Rust, Odin. 3 прекрасных языка, выбирай какой хочешь.

    Какая-то необычная пошлая тенденция на новые "низкоуровневые" языки, видимо всех ну очень задолбал C/C++.


  1. GrinEgor
    04.12.2024 07:33

    It's a trap