Jancy — скриптовый язык для системных/сетевых программистов

    jancyЗачем вообще создавать новый язык программирования? Их уже существует невероятное количество — по моему твёрдому убеждению, значительно больше, чем надо. И наверняка далеко не последнюю роль в данном положении вещей играет то, что создание компиляторов — это невероятно увлекательный процесс. С поправкой на арбузы и свиные хрящики — это вообще одна из самых «вкусных» работ, о которых может мечтать увлечённый программист.

    Непередаваемо здоровским является цветочно-конфетный период — первый этап изучения теории компиляторов по толстым умным книжкам, и — тут же! — её применения на практике, в своём собственном языке. Даже печальная перспектива того, что создатель языка вполне может оказаться его единственным пользователем, не способна перевесить радость творчества и остановить сферического-в-вакууме компиляторного Кулибина. Разумеется, если удовлетворение собственного интереса является не только важной, но и единственной движущей силой всего процесса — вышеописанная перспектива с неизбежностью будет воплощена в жизнь. Но даже если это и НЕ единственная причина создания нового языка — перспектива стать одиноким пользователем своего творения всё равно имеет шансы реализоваться.

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

    Так зачем же?


    Если максимально кратко сформулировать практическую сторону моей лично и нашей (как компании) мотивации, то она будет звучать так: мы хотели иметь встраиваемый скриптовый движок с указателями на структуры и безопасной адресной арифметикой. Такого не нашлось. И мы сделали язык Jancy («between-Java-and-C»), в котором есть и С-совместимые структуры, и указатели с безопасной арифметикой, и многое-многое другое:

    Уникальные возможности
    • Безопасные указатели и адресная арифметика;
    • Беспрецедентный для скриптовых языков уровень совместимости исходных кодов с C/C++;
    • Реактивное программирование (reactive programming) – одна из первых реализаций в императивном языке (а не в виде библиотек);
    • Встроенный генератор лексеров/сканнеров.

    Принципы дизайна
    • Объектно-ориентированный язык с C-подобным синтаксисом;
    • Бинарная ABI (application-binary-interface) совместимость с C/C++;
    • Автоматическое управление памятью через точную сборку мусора (accurate garbage collection);
    • LLVM в качестве backend.

    Другие значимые особенности
    • Исключения как синтаксический сахар над моделью кодов ошибок.
    • Свойства (properties) – самая полная реализация;
    • Мультикасты (multicasts) и события (events), включая слабые, от которых необязательно отписываться;
    • Множественное наследование;
    • Const-корректность;
    • Поддержка парадигмы RAII (resource-acquisition-is-initialization);
    • Локальная память потоков (thread local storage);
    • Частичное применение (partial application) для функций и свойств;
    • Оператор планирования (schedule operator) для создания указателей на функции, которые гарантированно будут вызваны в заданном окружении (например, в нужном рабочем потоке);
    • Перечисления для битовых флагов (bitflag enums);
    • Perl-подобное форматирование строк;

    Более полный список возможностей с примерами использования можно посмотреть тут: http://tibbo.com/jancy/features.html

    Кому это может пригодиться?


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

    1. Высокий уровень совместимости с C/C++
    Это относится и к бинарной ABI-совместимости, и к совместимости на уровне исходных кодов. Плюсов здесь много: это и бесшовное подключение существующих C-библиотек, и портирование кода с C/C++ с помощью копипастинга и последующих косметических правок (а иногда и вообще без них), и лёгкость создания на C/C++ новых библиотек для использования из Jancy-скриптов, и эффективность встраивания Jancy-движка в C/C++ приложение и т.д.

    2. Удобные средства для IO-программирования
    Тут я прежде всего говорю, во-первых, о поддержке указателей и адресной арифметики, идеально подходящих для разбора и генерации бинарных пакетов, и, во-вторых, о генераторе лексеров (причём инкрементальных, т.е. применимых к разбору IO потоков, приходящих по кускам). Сюда же можно отнести частичное применение и оператор планирования, которые вместе позволяют, например, создать обработчик завершения (completion routine) с захваченными контекстыми аргументами; при этом он будет автоматически вызван в нужном рабочем потоке.

    3. Удобные средства для UI-программирования
    Два слова: реактивное программирование. Уверен, что в ближайшем будущем поддержка реактивности — на уровне языка или же в форме костылей вроде препроцессоров и библиотек — станет неотъемлемой частью любой системы разработки пользовательских интерфейсов (UI). Jancy предлагает реактивность «из коробки», причём, по моему мнению, в совершенно интуитивной форме. Помимо реактивности, Jancy поддерживает всевозможные вариации свойств и событий, что также помогает строить красивые фреймворки пользовательского интерфейса.

    При этом, несмотря на примечательные возможности пункта номер три, мы пока не позиционируем Jancy как язык разработки пользовательского интерфейса. Задача-максимум на данный момент — стать скриптовым языком для низкоуровневого IO программирования, т.е. инструментом системного/сетевого программиста/хакера.

    А теперь — слайды! ©

    ABI-совместимость с C/C++


    Совместимость это всегда хорошо, а совместимость с де-факто языком-стандартом системного программирования — это просто здорово, не правда ли?

    Jancy-скрипты JIT-компилируются и могут быть напрямую вызваны из программы на C/С++, равно как и напрямую вызывать C/C++ функции. Это означает, что после правильного описания типов данных и прототипов функций в скриптах Jancy и приложении на C++ становится возможно передавать данные естественным образом, через аргументы функций и возвращаемые значения.

    Объявляем и используем функции из скрипта на Jancy:
    bool foo (
    	char charArg,
    	int intArg,
    	double doubleArg
    	);
    
    bar (int x)
    {
    	bool result = foo ('a', x, 3.1415);
    	// ...
    }
    

    Пишем реализацию на C/C++:
    bool foo (
    	char charArg,
    	int intArg,
    	double doubelArg
    	)
    {
    	// ...
    	return true;
    }
    

    Подключаем перед JIT-компиляцией скрипта:
    class MyLib: public jnc::StdLib
    {
    public:
    	JNC_BEGIN_LIB ()
    		JNC_FUNCTION ("foo", foo)
    		JNC_LIB (jnc::StdLib)
    	JNC_END_LIB ()
    };
    
    // ...
    MyLib::mapFunctions (&module);
    

    Готово! Никакой упаковки/распаковки variant-подобных контейнеров, явного заталкивания аргументов на стек виртуальной машины и т.д. — всё работает напрямую. На сегодняшний момент Jancy поддерживает все основные модели вызовов (calling conventions):
    • cdecl (Microsoft/gcc)
    • stdcall (Microsoft/gcc)
    • Microsoft x64
    • System V

    В обратную сторону — вызов Jancy из C++ — всё так же просто:
    typedef void Bar (int);
    Bar* bar = (Bar*) module.getFunctionByName ("bar")->getMachineCode ();
    bar (100);
    

    Как насчёт вызова системных функций и динамических библиотек (dll/so)? Не вопрос! Jancy предлагает бесшовную интеграцию с динамическими библиотеками:
    library User32
    {
    	int stdcall MessageBoxA (
    		intptr hwnd,
    		char const thin* text,
    		char const thin* caption,
    		int flags
    		);
    	// ...
    }
    
    // ...
    User32 user32;
    user32.load ("user32.dll");
    user32.lib.MessageBoxA (0, "Message Text", "Message Caption", 0x00000040);
    

    При этом разрешение имён будет производиться по мере обращения, а найденные адреса будут кэшироваться (напоминает DELAYLOAD, с поправкой на явную загрузку самого модуля). Обработка ошибок при загрузке и разрешении имён производится стандартным для Jancy методом псевдо-исключений (подробнее см. следующий раздел).

    Динамический поиск по имени (GetProcAddress/dlsym), разумеется, также возможен — хотя и не столь элегантен, как предыдущий подход.
    Пример
    typedef int cdecl Printf (
    	char const thin* format,
    	...
    	);
    
    jnc.Library msvcrt;
    msvcrt.load ("msvcrt.dll");
    
    Printf thin* printf;
    
    unsafe
    {
    	printf = (Printf thin*) msvcrt.getFunction ("printf");
    }
    
    printf ("function 'printf' is found at 0x%p\n", printf);
    

    Другим немаловажным следствием высокой степени совместимости между Jancy и C/C++ является возможность копипастить из общедоступных источников (таких как Linux, React OS или других проектов с открытым исходным кодом) и использовать определения заголовков коммуникационных протоколов на языке C:
    enum IpProtocol: uint8_t
    {
        Icmp = 1,
        Tcp  = 6,
        Udp  = 17,
    }
    
    struct IpHdr
    {
        uint8_t m_headerLength : 4;
        uint8_t m_version      : 4;
        uint8_t m_typeOfService;
        bigendian uint16_t m_totalLength;
        uint16_t m_identification;
        uint16_t m_flags;
        uint8_t m_timeToLive;
        IpProtocol m_protocol;
        bigendian uint16_t m_headerChecksum;
        uint32_t m_srcAddress;
        uint32_t m_dstAddress;
    }
    

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

    Псевдо-исключения


    Как это ни парадоксально, но одним из следствий ABI-совместимости с C/C++ стал отказ от привычной для C++ программистов модели исключений. Дело в том, что такие исключения совершенно не подходят для мультиязыкового стека вызовов (хотя, конечно, список объективных претензий к C++-подобным исключениям этим не исчерпывается — горячие споры «за» и «против» исключений всплывают на программистских ресурсах с регулярностью, которой можно только позавидовать).

    Так или иначе, в Jancy используется гибридная модель. В основе её лежит проверка возвращаемых значений, но компилятор избавляет от необходимости делать это вручную. В итоге всё выглядит почти как исключения в C++ или Java, но при этом поведение программы при ошибках на порядок более прозрачно и предсказуемо, а поддержка исключений при межязыковых взаимодействиях (таких, как вызов функций C++ из скриптов на Jancy и наоборот) становится настолько простой, насколько это вообще возможно.
    bool foo (int a) throws
    {
        if (a < -100 || a > 200) // invalid argument
        {
            jnc.setStringError ("detailed-description-of-error");
            return false;
    	}
    
        // ...
        return true;
    }
    

    Возвращаемые значения функций, помеченных модификатором throws, будут трактоваться как коды ошибок. В Jancy приняты интуитивные условия ошибки для стандартных типов: false для булева типа, null для указателей, -1 для беззнаковых целых, и < 0 для знаковых. Остальные типы приводятся к булеву (если это невозможно, то выдаётся ошибка компиляции). Очевидно, что функция, возвращающая void, в данной модели не может возвращать ошибки.

    Помимо этого, в данной модели разработчик волен выбирать, как именно обрабатывать ошибки в каждом конкретном случае. Иногда это удобнее делать проверкой кода возврата вручную, иногда – использовать семантику исключений. В Jancy — при вызове одной и той же функции! — можно делать и так и так, в зависимости от ситуации.
    bar ()
    {
        foo (10);   // can use exception semantics...
        foo (-20);
    
    catch:
        printf ($"error caught: $(jnc.getLastError ().m_description)\n");
        // handle error
    }
    
    baz (int x)
    {
        bool result = try foo (x); // ...or manual error-code check
        if (!result)
        {
    	    printf ($"error: $(jnc.getLastError ().m_description)\n");
            // handle error
        }
    }
    

    Конструкция finally в большинстве языков традиционно ассоциируется с исключениями. Но в Jancy finally может быть добавлен в любой блок по желанию разработчика. В конце концов, убирать за собой надо даже если никаких ошибок не возникало, не правда ли?
    foo ()
    {
         // nothing to do with exceptions here, just a 'finally' block to clean up
    finally:
        printf ("foo () finalization\n");
    }
    

    Конечно, допускается и более традиционное использование конструкции finally в случаях, когда исключения-таки ожидаются.
    Пример
    foo (char const* address)
    {
        try
        {
            open (address);
    
            transact (1);
            transact (2);
            transact (3);
    
        catch:
            addErrorToLog (jnc.getLastError ());
    
        finally:
            close ();
        }
    }
    


    Безопасные указатели и адресная арифметика


    Адресная арифметика в скриптовом языке — это то, ради чего всё собственно и затевалось.

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

    Чтобы всегда была возможность проверять корректность операций, указатели в Jancy по умолчанию толстые. Помимо адреса они также содержат валидатор – специальную структуру мета-данных, из которой можно получить информацию о разрешённом диапазоне адресов, типе данных, а также целочисленном уровне вложенности (scope level).

    Формула безопасности указателей и адресной арифметики в Jancy такова:
    1. проверка диапазонов при косвенных обращениях по указателям;
    2. проверка уровня вложенности при присвоениях указателей;
    3. проверка приводимости при присвоениях указателей.

    А как же производительность?
    Данный механизм не бесплатен и действительно выливается в определённые накладные расходы во время исполнения.

    Но во-первых, даже в самом наивном варианте, без каких-либо оптимизаций, два целочисленных сравнения для проверки диапазона или одно для проверки уровня вложенности – это не так страшно, особенно принимая во внимание JIT-компиляцию и тот факт, что Jancy – это всё-таки скриптовый язык.

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

    Проверки допустимого диапазона адресов производятся как в случае явного использования указателя, так и в случае оператора индексации:
    foo (
    	char* p,
    	size_t i
    	)
    {
    	p += i;
        *p = 10; // <-- range is checked
    
        static int a [] = { 10, 20, 30 };
        int x = a [i]; // <-- range is checked
    }
    

    В случае указателей на стековые и потоковые переменные, необходимы также проверки уровня вложенности — для предотвращения утечки адресов за границы времён их жизни. Этот механизм работает даже в случае многоуровневых указателей, вроде указателей-на-структуры-с-указателями-на-структуры-и-т-д:
    int* g_p;
    
    bar (
    	int** dst,
    	int* src
    	)
    {
    	*dst = src; // <-- scope level is checked
    }
    
    baz ()
    {
        int x;
        bar (g_p, &x); // <-- runtime error: scope level mismatch
    }
    

    Наконец, проверки приводимости призваны предотвратить разрушение самих валидаторов. Действительно, что если мы приведём создадим указатель на указатель, приведём его к указателю на char и затем байт за байтом затрём валидатор мусором? Jancy просто не даст это сделать: компилятор и runtime разрешают приведения только там, где это безопасно.
    Подробнее
    Jancy делит все типы на категории POD (plain-old-data) и не-POD. Понятие POD в Jancy несколько отличается от аналогичного в C++. Возможно, в этой связи стоило придумать новый термин, чтобы избежать путаницы, но в итоге я решил не плодить новые сокращения. Кроме того, мне кажется, что POD в Jancy гораздо точнее отражает смысл понятия “plain-old-data”.

    В Jancy POD – это данные без мета-данных. Их можно смело побайтно копировать и модифицировать и при этом ничего не сломать. Агрегация POD данных, будь то включения полей, наследование (тут отличие от C++) или объединение в массивы – тоже приводит к POD. Всё, что содержит мета-данные, а именно классы, безопасные указатели на данные и их любые агрегаты – это не-POD.

    Компилятор Jancy разрешает приведения не-POD типов тогда и только тогда, когда в результате приведения не появляется возможность разрушить или подменить мета-данные. Для ситуаций, в которых на этапе компиляции это неизвестно (например, мы производим приведение к дочернему типу, так называемый downcast) – существует специальный оператор динамического приведения. Оператор динамического приведения компилируется в вызов встроенной функции, которая возвращает указатель на запрошенный тип, или null, если приведение невозможно.

    Для примера подготовим тестовые типы, которые мы будем приводить друг к другу:
    struct A
    {
        int m_a;
    }
    
    struct B
    {
        int m_b;
    }
    
    struct C: A, B
    {
        int m_c;
    }
    
    struct D: C
    {
        char const* m_s;
    }
    

    Здесь A, B, C – это POD (причём последний тип не был бы POD в C++), D – не POD, т.к. этот тип содержит мета-данные в виде валидатора указателя m_s. Теперь рассмотрим возможные операции приведения.

    Приведения к родительским типам (upcast) всегда разрешены и не требуют явного оператора приведения ни для POD, ни для не-POD:
    foo (D* d)
    {
        C* c = d;
        A* a = с;
    }
    

    POD-типы могут быть произвольно приведены друг к другу с помощью оператора приведения:
    bar (B* b)
    {
        char* p = (char*) b;
        C* c = (C*) b; // <-- unlike C++ no pointer shift
    }
    

    Приведения от POD-типов к не-POD-типам разрешены только в случае результирующего константного указателя:
    foo (D* d)
    {
        char* p = (char*) d;  <-- error
        char const* p2 = (char const*) d; // OK
    }
    

    Приведение к дочерним типам (downcast) возможно с помощью оператора динамического приведения:
    bar (B* b)
    {
        D* d = dynamic (D*) b;
        A* a = dynamic (A*) b; // not a downcast, but still OK
    }
    

    Динамическое приведение возможно благодаря содержащемуся в указателе валидатору, а значит и информации о типе. Помимо динамического приведения, Jancy также предлагает операцию динамического определения размера, который доступен из того же валидатора — хотя это и не имеет отношения к безопасности указателей, в определенных ситуациях это очень удобно:
    foo (int* p)
    {
        size_t size = dynamic sizeof (*p);
        size_t count = dynamic countof (*p);
    }
    
    //...
    bar ()
    {
    	int a [100];
    	foo (a);
    }
    


    Уважаемые хабровчание приглашаются поиграться с нашим online-компилятором и на деле опробовать, как всё это работает (читайте: попытаться подсунуть компилятору пример скрипта с указателями, который его свалит ;)

    Подробнее про указатели в Jancy читать тут: http://tibbo.com/jancy/features/pointers.html

    Функции-автоматы


    Безопасные указатели и адресная арифметика идеально подходят для разбора и генерации бинарных пакетов:
    dissectEthernet (void const* p)
    {
    	io.EthernetHdr const* hdr = (io.EthernetHdr const*) p;
    	switch (hdr.m_type)
    	{
    	case io.EthernetType.Ip:
    		dissectIp (hdr + 1);
    		break;
    
    	case io.EthernetType.Ip6:
    		dissectIp6 (hdr + 1);
    		break;
    
    	case io.EthernetType.Arp:
    		dissectArp (hdr + 1);
    		break;
    
    	// ...
    	}
    }
    

    Но существует и другой класс протоколов — протоколов, не полагающихся на бинарные заголовки и вместо этого использующих некий язык запросов/ответов. В этом случае для анализа IO-потоков требуется писать парсер данного языка. При этом надо озаботиться предварительной буферизацией данных — часто нет гарантии, что транспорт доставил сообщение целиком, а не по кусками.

    Поскольку данная задача является типовой в IO-программировании, Jancy предлагает встроенный инструмент для её решения. Функции-автоматы Jancy призваны облегчить первую и самую рутинную стадию написания любого парсера — создание лексера/сканнера. Работает это по принципу известных лексер-генераторов типа Lex, Flex, Ragel:
    jnc.AutomatonResult automaton scanRx (jnc.Recognizer* recognizer)
    {
    	%% "getOption"
    		createToken (Token.GetOption);
    
    	%% "setOption"
    		createToken (Token.SetOption);
    
    	%% "exit"
    		createToken (Token.Exit);
    
    	%% [_\w][_\w\d]*
    		createToken (Token.Identifier, recognizer.m_lexeme);
    
    	// ...
    }
    

    Внутри функции-автомата описан список распознаваемых лексем в виде регулярных выражений. После описания каждой лексемы следует блок кода, который надо выполнить при её обнаружении во входном потоке. Вся эта кухня компилируется в табличный ДКА, состояние которого хранится во внешнем объекте jnc.Recognizer (указатель на этот объект передаётся в аргументе recognizer). В нём же накапливаются символы потенциальной лексемы, и он же неявно вызывает нашу функцию-автомат, выполняя при этом необходимые переходы между состояниями.

    Совокупность функции-автомата и этого управляющего объекта-распознавателя и представляет собой наш лексер. При этом данный лексер будет инкрементальным, то есть способным разбирать сообщения, приходящие по частям:
    jnc.Recognizer recognizer (scanRx); // create recognizer object
    
    try 
    {
        recognizer.write ("ge");
        recognizer.write ("tOp");
        recognizer.write ("tion");
        recognizer.eof (); // notify recognizer about eof (this can trigger actions or errors)
    
    catch: 
        // handle recognition error
    }
    

    Отметим, что, как и в Ragel, возможно переключаться между разными функциями-автоматами, что позволяет, в частности, создавать контекстно-зависимые ключевые слова (или, если сказать по-другому, разбирать мульти-языковый вход).
    Пример
    jnc.AutomatonResult automaton scanGlobal (jnc.Recognizer* recognizer)
    {
    	%% '#'
    		recognizer.m_automatonFunc = scanPreprocessor; // switch to pp-specific keywords
    
    	// ...
    }
    
    jnc.AutomatonResult automaton scanPreprocessor (jnc.Recognizer* recognizer)
    {
    	%% "if"
    		createToken (Token.If);
    
    	%% "ifdef"
    		createToken (Token.Ifdef);
    
    	// ...
    	%% '\n'
    		recognizer.m_automatonFunc = scanGlobal; // switch back
    }
    

    Функции-автоматы с одной стороны, и безопасные указатели с адресной арифметикой с другой позволяют с удобством разбирать протоколы и IO-потоки любого типа.

    Заключение


    Несмотря на то, что программирование пользовательских интерфейсов (UI) не является главным назначением Jancy на данный момент, мы всё равно хотели бы продемонстрировать подход к реактивному программированию, который используется в Jancy — я считаю, что нам удалось найти оптимальный компромисс в сосуществовании императивного и декларативного начал в реактивном программировании. Рассказ об этом пойдёт в следующей статье.

    Тем временем мы приглашаем вас опробовать возможности языка Jancy (как описанные в данной статье, так и многие другие) на страничке живой демки компилятора. Также вы можете скачать, собрать и поиграться с библиотекой JIT-компилятора Jancy и примерами её использования — всё это доступно на страничке downloads.
    Tibbo
    26,00
    Компания
    Поделиться публикацией

    Комментарии 24

      +3
      Почему вы называете его — язык для системных/сетевых программистов?
      В тексте не смог найти пояснений на этот счет?

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

      Как планируете развивать и строить комьюнити?
        +1
        Почему вы называете его — язык для системных/сетевых программистов?
        В тексте не смог найти пояснений на этот счет?

        Тут важно уточнить. Jancy — не язык системного программирования, а скриптовый язык для системного/сетевого программирования. По области применимости он целит не в Rust, а в Питон. А пояснения были. Он может быть удобен для системного/сетевого программирования (как минимум, в ряде сценариев) по следующим причинам:

        • поддержка указателей на данные и адресной арифметики (самый лучший способ «гулять» по бинарным данным для анализа или генерации)
        • ABI-совместимость с C/C++ бибилотеками (вызывать системные функции проще чем в любом другом скриптовом языке)
        • Высокий уровень совместимости исходных кодов в с C (иногда можно вообще копипастить код и алгоритмы на С)

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

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

        Это смотря откуда эти программисты пришли ;) Я всегда наоборот искал что-то си-подобное и подозреваю, что я в этом не одинок.

        Python в этом вопрос значительно проще и уже набрал хорошую популярность в среде сетевиков.

        Про простоту — см выше, си-подобные языки действительно имеют более высокий порог вхождения. Но это не столь релевантно, если человек УЖЕ приходит из си-мира. Jancy ориентируется именно на таких. А вот про популярность Питона у сетевиков — совершенно справедливо подмечено! Навряд ли я смогу это доказать, но думаю, что, одна из немаловажных причин этой популярности — это самый простой среди скриптовых языков вызов си из Питона. Так вот, Jancy идёт в этом направлении ещё дальше.

        Как планируете развивать и строить комьюнити?

        Это вопрос на миллион :) Как минимум — начать рассказывать, что вот-де есть такой скриптовый язык с указателями, поездить по конференциям, а в итоге мы, конечно, хотели бы найти увлечённых разработчиков, заинтересованных обсуждать, спорить, пробовать, а в идеале и присоединиться к работе над проектом.
      • НЛО прилетело и опубликовало эту надпись здесь
          +1
          Да как минимум для поднятия своего скила.
            0
            Жаль что столько минусов — не думаю, что ваш коммент этого заслуживает. Я совершенно согласен, что языков понаваяно уже ого-го сколько, о чём и написал в самом первом абзаце. А то ли ещё будет! ;) Достаточность, однако, вопрос не такой однозначный. Всегда есть что улучшить, и всегда найдётся горячая голова, которая попробует это сделать. В нашем случае самый краткий ответ на вопрос «зачем?» — это создание скриптового языка с указателями и безопасной адресной арифметикой — такого пока не было.

            Кстати, в Rust мы не метим. Думаю, что в будущем Rust (или что-то подобное) будет потихоньку теснить си на ниве системного программирования — это язык с по-настоящему прорывными концепциями в области управления памятью. У нас же — скриптовый язык. Мы метим в Питоны ;)
            0
            Неплохая «обертка» над LLVM и пахнет действительно вкусно, по мне нехватает пока некоторых «прянностей» других скриптовых (например прямое substituation как функция, доступ в верхний scope, чтобы например создать свои «функции» для передачи body прямо в языке без биндинга, так же lambda expressions, что то типа closure, и т.д.), но оно придет с развитием… (а возможно просто я сразу не нашел).

            И кое-что, естественно не нравится, например catch внутри scope try (типа прямой llvm метки?). Хотя о вкусах не спорят.
              0
              Ах да, а есть где scm какой (или где на гитхабе и т.д. можно бы взглянуть)?
                0
                Исходники выложены на нашем сайте, про переехать на гитхаб — подумываем.
                  0
                  Да я видел, scm не совсем исходники…
                    0
                    Скорее всего, в определённый момент мы просто переедем на гитхаб. К сожалению, у меня нет опыта руководства опенсорсными проектами, поэтому имеются естественные опасения в связи с переездом и открытием проекта для редактирования сообществом. С другой стороны, как говорил в одном интервью автор D Волтер Брайт, одна из вещей, о которых он жалеет — это то, что не отдал D сообществу раньше :)

                    По поводу вкусностей, присутствующих в других скриптовых языках: лямбды и замыкания со временем обязательно появятся. Пока в качестве альтернативы настоящим замыканиям можно использовать частичное применение — в Jancy можно захватывать произвольные контекстные аргументы в момент создания указателя на функцию.
                      +1
                      поэтому имеются естественные опасения в связи с переездом и открытием проекта для редактирования сообществом.
                      А никто его не сможет редактировать, кроме тех людей кому вы это специально разрешили, изначально это может делать только owner. И это нормальная практика — контрибуция же от коммюнити идет через pull-request. Т.е. человек создает форк, правит там у себя что-то и отправляет запрос (pull-request) в вашу репо. А дальше уже вам решать.
                      Кроме того если у вас так и так git, то ваш локальный repo вообще не затрагивается если даже на гитхабе вообще все слетит или перепишется. (Оба репозитария равноценны) В вашем локальном репо, после fetch вы просто увидите что на гитхабе gh-origin ветки пропали, или были созданы новые. Ваше же все останется так и так, даже если будет fetch с выставленным prune (почистить удаленные ветки), в ref-log вы сможете найти и восстановить эти ветки еще некоторое время (пара недель если не ошибаюсь). Хотя обычно даже это не нужно, просто восстанавливается из другого репо, в котором они еще остались. Вплоть до полного пересоздания.
                      За свою долгую жизнь в опенсорсе я не слышал ни разу, чтобы кто-нибудь потерял чего-либо на git, fossil, hg, svn, cvs. Последние два с оговоркой, т.к. централизованы (если репо на сервере слетело — пиши пропало).
                        0
                        Спасибо за развёрнутый комментарий. Да, внутри компании в качестве version control мы используем git. Под опасениями я имел в виду скорее не то, что код на гитхабе пропадёт, а то, что я пока не до конца представляю себе, как заниматься управлением пулл-реквестов от совершенно незнакомых мне людей. Но ничего, разберёмся :)
              0
              Это все наверно хорошо, но вот долго думал где бы я это применил. И понял что нигде.
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  Это погодные условия на хабре ;)
                0
                Насколько я люблю самописные языки, но с этим как-то не понял, в чем суть. Хочется больше примеров именно уникальных фич языка и сценариев, в которых он удобнее существующих аналогов.
                  0
                  Суть в близости нашего скриптового языка к си. Можно сказать, у нас скриптовый си с плюшками. Уникальных фич достаточно много (мы, на самом деле, подзатянули с «выходом в свет» с допиливанием уникальных фич)

                  • безопасная адресная арифметика
                  • встроенный генератор лексеров
                  • реактивное программирование на уровне языка

                  И много нововведений по мелочи, как-то встроенная поддержка бигендианов, енумы для битовых флагов, указатели на функции с отложенным исполнением и другое. Здесь подробнее и с примерами: http://tibbo.com/jancy/features.html
                  0
                  Неплохая задумка. Работать еще надо и, судя по всему, немало, но что-то в этом есть. Возможно, будет жить.
                    0
                    Псевдо-исключения, коды возврата и try/catch — это какая-то каша. И вообще, скриптовый язык с кодами возврата — это жуть. Вы-же LLVM используете. Не смогли выяснить как там генерировать код выбрасывающий и ловящий исключения? К тому же для x86-64 существует ABI стандарт, строго определяющий бинарный интерфейс для исключений. Убедите меня, что ваш язык лучше Python-а. В питоне есть исключения (и лямбды, и ещё чего всякого), а у вас нет.
                      +2
                      Псевдо-исключения, коды возврата и try/catch — это какая-то каша. И вообще, скриптовый язык с кодами возврата — это жуть. Вы-же LLVM используете. Не смогли выяснить как там генерировать код выбрасывающий и ловящий исключения? К тому же для x86-64 существует ABI стандарт, строго определяющий бинарный интерфейс для исключений.

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

                      Для примера — код, генерируемый llvm 3.3 не уживался с g++ 4.8.2 на x86_64: в определённый случаях c++ (1) -> jancy (2) -> c++ (3) exception из с++ (3) не ловился в с++ (1). А когда стек вызовов вообще мультиязыковый, использовать плюсовые исключения — ну ненадёжно. Самый безопасный и прозрачный подход — коды возврата и потоковые переменные. Над этим насыпано синтаксического сахара, чтоб не было «жути» ;)

                      Убедите меня, что ваш язык лучше Python-а. В питоне есть исключения (и лямбды, и ещё чего всякого), а у вас нет.

                      ОК, попробую :) Исключения в Jancy есть, но в виде синтаксического сахара. Лямбды будут. А вот что у нас уже сейчас лучше чем в Питоне, так это работа с бинарными данными: безопасная адресная арифметика и инкрементальный разбор регекспами. И интеграция с си бесшовная, лучше чем в питоне. А ещё у нас есть реактивное программирование ;)

                      Но вообще вы абсолютно правы — сравнивать надо с Питоном. Это наша область применимости.
                        0
                        А зачем пробрасывать исключения через границу jancy->c++ или c++->jancy? Подход, который отлично работает не только в случае разных языков, но и просто разных модулей — это ловить все исключения на границе между языками/модулями и преобразовывать их в ошибки, которые понимаются другим языком/модулем. А после перехода границы, можно снова выбрасить исключение имеющее смысл уже для данного модуля. В общем, мне кажется, что ваша «ненадёжность» исключений основывается на их неправильном использовании.

                        Далее, с кодами возврата не всё гладко. Во первых, у вас появляется соглашение о том, что является ошибкой (null, -1, etc), а что нет. Во вторых, типа ошибки нигде нет. Напротив, в Питоне, например, тип ошибки — это тип исключения, и разные типы ошибок можно обрабатывать по-разному. В третьих, синтаксический сахар для обрабоки ошибок мне показался неудобным. Как пробросить ошибку через несколько уровней вызовов и сделать так, чтобы код промежуточных уровней остался не затронутым, т.е. не знал вообще что на нижнем уровне происходит ошибка, а на верхнем она обрабатывается? В вашем подходе на каждом промежуточном уровне прийдётся писать try.
                          0
                          А зачем пробрасывать исключения через границу jancy->c++ или c++->jancy? Подход, который отлично работает не только в случае разных языков, но и просто разных модулей — это ловить все исключения на границе между языками/модулями и преобразовывать их в ошибки, которые понимаются другим языком/модулем. А после перехода границы, можно снова выбрасить исключение имеющее смысл уже для данного модуля. В общем, мне кажется, что ваша «ненадёжность» исключений основывается на их неправильном использовании.

                          ОК, значит возникло недопонимание. Вы говорили про ABI-стандарт для исключений и я понял так, что предлагается ловить и бросать плюсовые исключения. Хорошо, значит мы сходимся в главном: исключения — в каком бы виде они ни были реализованы — на границах языков надо преобразовывать. Так вот, Jancy предлагает модель исключений, которая является *самой простой* при организации межязыковых взаимодействий. Причём в обе стороны: поймать Jancy-исключение = проверить код возврата и считать расширенную информацию об ошибке из TLS; бросить Jancy-исключение = записать расширенную информацию в TLS и вернуть «ошибочный» код возврата.

                          Далее, с кодами возврата не всё гладко. Во первых, у вас появляется соглашение о том, что является ошибкой (null, -1, etc), а что нет.

                          Совершенно верно. «Бросающие» функции должны следовать определённому протоколу.

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

                          Вступаю на взрывоопасную поверхность, поэтому дисклеймер: нижеследующее является не более, чем моим личным мнением ;) Я считаю, что сама концепция писать разные блоки catch под разные типы исключений — глубоко порочна. По сути стандартные типы исключений решают искусственную проблему — в блоке catch мы как правило хотим ловить *любые* исключения. Безусловно, вы можете придумать примеры, в которых надо проверять тип исключения и обрабатывать их в разных catch-ах. Но эти примеры всегда можно переписать так, что блок catch останется один. А во-вторых, подход Jancy ничему не противоречит — всегда можно проанализировать расширенную информацию об ошибке из TLS и построить соответвующее ветвление.

                          В третьих, синтаксический сахар для обрабоки ошибок мне показался неудобным. Как пробросить ошибку через несколько уровней вызовов и сделать так, чтобы код промежуточных уровней остался не затронутым, т.е. не знал вообще что на нижнем уровне происходит ошибка, а на верхнем она обрабатывается? В вашем подходе на каждом промежуточном уровне прийдётся писать try.

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

                          Безусловно, данная модель не универсальна и имеет ряд ограничений — например, нельзя бросать исключения из void-функций или функций, возращающих некие структуры. Обойти всё это можно, несколько подправив прототипы «бросающих» функций — так, чтобы они удовлетворяли протоколу псевдо-исключений Jancy. В целом же, согласитесь, ограничения присущи любой модели, и для эффективного её использования требуется некоторым образом «отформатировать» под неё мозг — уж к плюсовым исключеним это относится в полной мере ;)
                      +3
                      Языки разрабатываются потому, что существующие чем то не устраивают. Это было, есть и будет всегда. Можно спорить конечно по поводу, насколько актуальны цели создания языка и как они достигнуты.
                      По моему цели сформулированы правильно и слабые стороны существующих языков определены верно. Языки послужившие базой это Си и Java. В Си адресная арифметика явно слабое место. Вообще ссылки основной источник ошибок. А в больших проектах это просто кошмар. Java пыталась с этим бороться но превращение всего в объекты меня лично напрягает. Я считаю, что проект интересный. Мне кажется, что если бы это был транслятор, а не интерпретатор то перспективы у него было бы значительно больше.
                        0
                        Спасибо! Только Jancy — не интерпретатор, а именно транслятор. Пока это JIT-компилятор, а сохранение исполнимых бинарников — в планах и со временем допилится.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое