Для начала стоит заметить тот факт, что я в целом являюсь личинкой программиста, и не умею писать чистый код по определению. По этому вся статья построена на принципе “мне стало скучно, я решил сотворить какой то дичи, я чуть чуть поискал официальное решение, не нашел и пошел строить замок из костылей (лезть в C код который генерит компилятор же сойдет за костыли, или это типо официальное решение?)”

Вообщем, не так давно я побывал на летней школе CTF 2025 года, и мягко скажем, мне очень понравилось. Но были там две лекции, которые мне запомнились особенно ярко и запали в душу. А именно лекции админов канала ворчалки о программировании Дмитрия Соломенникова и Алексея Недори про безопасность компиляторов и разработку языков программирования.

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

По этому, по возвращению домой и после 20 часов сна сон (программа на ЛШ была, мягко скажем, насыщенная, и спать было особенно некогда)

я полез читать про разработку языков, но, к сожалению, 1200 страниц “Книги дракона” как ее назвали где то на хабре, за пару дней не осилить (но я надеюсь дочитать ее хотя бы к октябрю).

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

Тут стоит сделать сноску. Во первых статья только про тривиль. Я не трогал Арс и Арвиль, и будет забавно если окажется, что он был создан как раз решения тех проблем, которые я себе создал. Во вторых - То, как я читал исходники компилятора останется за кадром. Тут только спутаный поток сознания про User expirience от самого языка

Часть 1. Как театр начинается с вешалки, так изучение нового ЯП начинается с настройки#

Установка в целом крайне простая. Надо просто скачать архив с релизов, распаковать и пользоваться. И все прекрасно работало бы, если бы многолетний опыт сидения на винде… Дело вот в чем. Почему то компилятор Тривиля упорно выплевывает файлы только с расширением .exe вне зависимости от того, под чем оно собирается. И Юниксу на это плевать, ведь он обозначает форматы файла по сигнатуре в начале, но я потратил минут 20, удивляясь столь великолепным возможностям языка по кросс компиляции под другие платформы. вот так вот Пока не догадался исполнить .exe файл из релиза под линукс под orbstackом, и все заработало (и да, все свои издевательства я проводил на макбуке на M1) где это видано что бы .exe был MachO

Итак, программа скомпилирована, вечер за написанием циклов, классов и прочих простых конструкций проведен, класс. Но тут мне стало скучно. А когда мне становится скучно, это плохо заканчивается для того, на чем я пишу.

Но перед этим, стоит упомянуть слона в комнате

Часть 2. Великий и могучий#

Для начала стоит уточнить. Я пользуюсь достаточно компактной клавиатурой. Тем не менее, по раскладке она вроде бы никак не отличается от обычных клавиатур за исключением того, что в русской раскладке запятая и точка ставятся на shift + 6 и shift + 7 соответственно (вот что бывает с теми кто продался в яблочное рабство).

моя прелесть И вот знаете, у нас, у молодого поколения (мне 16 кстати), 1с уже стал как шутка, потому что код на русском выглядит непривычно, и смотрится странно. Опыт тривиля убедил меня, что это еще и не всегда удобно.

Первое, и самое бесящее - VIM начинает работать некорректно. Я сижу не на винде, по этому nodepad++ для меня закрыт, а подсветку синтаксиса я заставил работать только под вимом. При этом вим не очень любит русскую раскладку, и тут либо менять ее при каждом уходе в normal mode, либо искать решение по тому как это делать автоматически. Проверив первые две ссылки в гугле я не смог найти рабочего решения, а значит его не существует по сему просто решил доблестно страдать.

Второе - спецсимволы. Синтаксис языка богат на @, ::, и {}, которых нет в русской раскладке. И если с отсуствием собаки и двоеточий еще можно смириться, ведь используются они не так часто, то вот фигурные скобки чуть чуть нервируют. имхо синтаксис Ruby или кумира с его бесконечными рядами из end/нц-ку был бы чутка удобнее.

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

единственное, что хотя бы минимального LSP сервера не хватает…

UPD от 18 августа. Я сел писать LSP, и вроде бы даже получается. Надеюсь сделать его в разумные сроки

Часть 3. Издевательства и мнооооого костылей#

Итак, вернемся к тому, что мне стало скучно. Есть у меня традиция - с каждым новым языком писать небольшое веб приложение (все таки я веб разработчик в основном). Так что я решил провернуть шалость и тут

Поскольку готовых веб фреймворков для тривиля я почему то не нашел :( было принято решение сделать свой. В стд ничего связанного с http или TCP найдено небыло, по этому путь мой лежал к написанию своей библиотеки на Си и попыткам подружить ее с тривилем.

И стоит заметить, что для этого в языке есть все необходимое. А именно @внеш в качестве тела функции.

После исследования выяснилось, что компилятор на уровне Си просто транслирует это в вызов функции без ее определение, оставляя определене на совести разработчика. Насколько я понял, сделано это было для использования методов рантайма. Но нам ничего не мешает использовать это в своих гнусных целях. Прикол в том, что компилятор генерирует Си код и скрипт build.sh который собирает все это в бинарь. И нам ничего не мешает добавить в этот билд скрипт нужные нам файлы.

собсно, так и была рождена вот эта партянка на баше, которая и делала всю работу

#удаляем директорию что бы оно собирало исходник с нуля, а не используя существующий код
rm -rf ./_си/
# генерируем Си код при помощи компилятора. кстати этот билд закончится ошибкой. Это не баг, а фича
./трик.exe code
# добавляем наши файлики
cp ./code/socket.c  ./_си/socket.c

# добавление хедера кстати ломает сборку :D
#cp ./code/socket.h ./_си/socket.h

# добавляем их в билд скрипт
echo [ТУТ ОЧЕНЬ ДЛИННАЯ СТРОКА ВЗЯТАЯ ИЗ ОРИГИНАЛЬНОГО СКРИПТА С ДОБАВЛЕНИЕМ socket.c] > ./_си/build.sh
# триггерим сборку. прикол в том что она собирает только Си файлы но не генерит новый код из .tri файлов
./_си/build.sh

и теперь мы можем вызывать фукнции из нашего .tri файла

фн some_name(port: Цел64): Цел64 @внеш
int some_name(port: int) {
return 2;
}

по этому за пару часов было написано сие чудо. Сделано это было в целях демонстрации из разряда “Смари шо могу” по этому тут чуть чуть награмождено костылей. Одно только чтение буфера в TString чего стоит.

АХТУНГ. ЭТО ОЧЕНЬ ПЛОХОЙ КОД. НЕ НАДО ТАК

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include "rt_api.h"


int create_socket_fd(int port) {
    int server_fd;
    struct sockaddr_in address;
    int opt = 1;

    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        perror("setsockopt SO_REUSEADDR");
        exit(EXIT_FAILURE);
    }

#ifdef SO_REUSEPORT
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)) < 0) {
        perror("setsockopt SO_REUSEPORT");
        exit(EXIT_FAILURE);
    }
#endif

    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(port);

    if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    return server_fd;
}

int accept_socket(int fd) {
    int server_fd = fd;
    struct sockaddr_in client_addr;
    socklen_t addrlen = sizeof(client_addr);

    int new_socket = accept(server_fd, (struct sockaddr*)&client_addr, &addrlen);
    if (new_socket < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    return new_socket;
}

void write_string(int socket, TString data) {
    char* datastring = data->body;

    write(socket, datastring, strlen(datastring));
}
void close_socket(int socket){
    close(socket);
}

TString read_to_string(int socket) {
    char buffer[1024] = { 0 };
    TString string;
    read(socket, buffer, 1024 -1);
    TString  result = tri_newString(sizeof(buffer), sizeof(buffer), buffer);
    return result;
}
//int main(){
//    int socket = create_socket(8080);
//    //TString buf = read_buffer(socket);
//    //printf("%s", buf->body);
//}

Единственное, что вместо стандартных char* или std::string тут TString. благо в рантайме есть метод tri_newString() который позволяет создавть эти самые строки. Но, когда я пытался написать либу на Rust, подружить их у меня пока не получилось, хотя видимо это дело техники

Теперь осталось все это просто собрать и запустить. И получить ошибку

вот вам мем примерно так ощущалась сборка

Вернее ошибку не при сборке, а сегфолт Вот это чудо

И я пытался понять, что за прикол очень долго, пока не обратил внимание на то, что пишет clang

/Users/greenhaze/Downloads/trivil-linux-v0.79 /Users/greenhaze/Downloads/trivil-linux-v0.79
ttcp_server.c:9:16: warning: implicit declaration of function 'create_socket_fd' is invalid in C99 [-Wimplicit-function-declaration]
TInt64 soket = create_socket_fd(8080);
               ^
ttcp_server.c:11:21: warning: implicit declaration of function 'accept_socket' is invalid in C99 [-Wimplicit-function-declaration]
TInt64 slushatel_ = accept_socket(soket);
                    ^
ttcp_server.c:12:30: warning: implicit declaration of function 'read_to_string' is invalid in C99 [-Wimplicit-function-declaration]
TString poluchennye_dannye = read_to_string(slushatel_);
                             ^
ttcp_server.c:12:9: warning: incompatible integer to pointer conversion initializing 'TString' (aka 'struct StringDesc *') with an expression of type 'int' [-Wint-conversion]
TString poluchennye_dannye = read_to_string(slushatel_);
        ^                    ~~~~~~~~~~~~~~~~~~~~~~~~~~
ttcp_server.c:14:1: warning: implicit declaration of function 'write_string' is invalid in C99 [-Wimplicit-function-declaration]
write_string(slushatel_, tri_newLiteralString(&strlit1, -1, 3, "да."));
^
ttcp_server.c:15:1: warning: implicit declaration of function 'close_socket' is invalid in C99 [-Wimplicit-function-declaration]
close_socket(slushatel_);
^
6 warnings generated.
/Users/greenhaze/Downloads/trivil-linux-v0.79

и прикол в том, что ошибки формата implicit declaration of function начали вылезать с того момента, как я начал тыкать палкой в вывод компилятора, а вот integer to pointer conversion warning наконец-то дал мне понять, что оно не подтягивает прототипы функций. Почему-то. Я попытался добавить .h файлик, но был послан в пешее эротическое undefref Пока самым рабочим решением даннной проблемы является добавление прототипов прямо в файл где находится функция main руками. Да, я знаю, что костыль, но пока это единственный рабочий вариант. (и да, я допускаю, что я слепой и не увидел нормального способа как это сделать в документации и статьях, которые я естессно прочитал.)

И таким образом, мы наконец то можем написать простенький сервер, и получить заветное Hello World в браузере

Победа? Если не учитывать моих затупов с sizeof/strlen, то да, собрав и открыв браузер мы увидим нашу шедевространичку

модуль тцп_сервер

импорт "стд::вывод"

фн create_socket_fd(port: Цел64): Цел64 @внеш
фн accept_socket(fd: Цел64): Цел64 @внеш
фн read_to_string(fd: Цел64): Строка  @внеш
фн close_socket(socket: Цел64)@внеш
фн write_string(socket: Цел64, data: Строка) @внеш

фн старт_сервер(){
пусть сокет = create_socket_fd(8081)
пока истина {
пусть слушатель = accept_socket(сокет)
пусть полученные_данные = read_to_string(слушатель)
вывод.строка(полученные_данные)
// да, это чистый Http ответ. Я пока еще только в процессе написания парсера http, но в целом для демонстрации пойдет
пусть хттп_строка = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><body><h1>Hello, World!</h1><p>This is a simple HTTP server in Trivil.</p></body></html>"
write_string(слушатель,хттп_строка)
close_socket(слушатель)

}
}

вход {
	старт_сервер()
}

ITS ALIVE

Кстати, в STD уже есть модули для работы с JSON.

Разве что в STD вообще нет упоминания многопоточности… Для меня это повод погрузится в устройство async/await/multithreading в других языках что бы добавить это все сюда

Часть 4. А дальше то что?#

В целом, опыт, который я получил, мягко скажем, интересный. Вообще было бы неплохо развить этот небольшой статический сервер в хоть сколько то полноценный HTTP фреймворк, и я обязательно этим займусь, потому что сервисы на Attack-Defense соревнования писать надо, а за одно только упоминание эрланга в этом контексте мне уже угражают распятием. Так что ждите на какой-нибудь адшке сервис на тривиле от меня ;)

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

UPD от 18 августа. Прошли выходные, и я понял, что тривиль великолепно подходит как то, чем иногда описывают Python, или же фронтенд для бекенда. Это первый язык, где FFI настолько прост, и я считаю @внеш настоящей киллер фичей языка, в которую постепенно начинаю влюблятся. Пока что в планах еще поработать над инфраструктурой, написать хотя бы базовую пародию на Gin (раз уж в языке столько отсылок на go), а потом еще позаниматся некромантией, например попытатся собрать тривиль под Raspberry pi pico (которая микроконтроллер)

автору языка во первых огромное спасибо за лекцию, а во вторых спасибо за то, что во много раз усилили мой интерес к языкам (я надеюсь это надолго), а в третьих за головоломку под названием Тривиль

На этом вроде все. Коли вопросы и предложения будут, найти меня можно в телеграмме

Всем хорошего дня!