Pull to refresh

Язык программирования o42a

Reading time 15 min
Views 8.3K
Я не люблю программировать. Мне нужен результат.

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

Но главное — чтоб заработало. И чем раньше — тем лучше.

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

Так вот. Современные языки программирования мешают излагать мысли, обременяя множеством деталей, не относящихся к решаемой задаче, и требующихся лишь для того, чтобы до транслятора языка «дошло». И речь даже не о синтаксисе, хотя многие, особенно компилируемые, языки чрезмерно многословны. Речь, прежде всего, о языковых сущностях, что и есть те «термины», в которых должно изъясняться. Эти сущности — например функции, переменные, классы, методы, пакеты, пространства имён, обобщения, шаблоны — слишком узкие, специализированные, предназначенные больше для машинного представления, нежели для человеческого понимания. Они заставляют переводить мысли на их язык. Это не сложно, конечно. Но это совершенно не относится к решаемой задаче. Выбор подходящих языковых сущностей и перевод на них отвлекают от задачи, снижают концентрацию и, следовательно, снижают эффективность разработки. И, я подозреваю, существенным образом. Понимание же сути в процессе чтения такого кода ещё более затруднительно, что также не лучшим образом влияет на производительность труда, особенно при командной разработке.

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

Основная идея o42a — автоматизировать труд программиста. И достигается это путём радикального сокращения видов языковых сущностей до одного-единственного, способного непосредственно заменить их все. Задача же эффективного машинного представления такой сущности целиком ложится на компилятор.

Замысел


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

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

Идею много лет назад подсказал мне Пролог. Помимо магии исчисления предикатов, есть в нём ещё одна поразительная особенность: глядя на запись предиката, можно увидеть, что одновременно — это запись обычной функции. Рассматривать можно и так и эдак, суть не меняется.

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

Вторая идея o42a — это возможность разделения языковой семантики и синтаксиса. Синтаксис не обязан один-в-один соответствовать языковым сущностям, сколь бы универсальны они ни были. Синтаксис должен выражать семантику программы, а не языка программирования. Он должен быть удобен для восприятия и достаточно строг. Это задача компилятора, а не программиста, приводить текст программы к языковым сущностям.

Можно сопоставить синтаксис с представлением, а программные сущности — с моделью из парадигмы MVC. А можно назвать это DSL.

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

Основы синтаксиса


Я решил, что лучший синтаксис придуманный человечеством — это письменная речь. Так что чем ближе ситаксис языка будет к письменной речи (английской, разумеется, по сложившейся в программировании традиции), тем лучше. В конце концов, выражение «читаемость кода» предрасполагает к такому выбору, ведь письменная речь и создавалась, чтобы быть читаемой, а читать её учат с детства.

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

Имена

Имена в o42a регистронезависимы и состоят из слов, латинских чисел и дефисов, отделяемых друг от друга пробелами:
Hello World
Links 2- 3- 4
Т-90а

Буквы в словах — это любые буквы уникода. Несколько пробелов подряд означают то же, что и один пробел. Пробелы не обязательно ставить между буквами и не-буквами, цифрами и не-цифрами. Имя не должно начинаться с цифры или дефиса и не должно заканчиваться дефисом. Дефисы должны быть одинарными. Пробел перед дефисом запрещён, дабы отличить его от знака вычитания.
Так что имена выше можно записать иначе:
Hello     world
links2-3-4
Т-90 А

Комментарии

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

Строчный комментарий начинается двумя или более тильдами:
a + b ~~ The sum

Заканчивается строчный комментарий либо в конце строки, либо двумя или более тильдами:
a +~~~plus~~~ b

Блочный комментарий начинается и заканчивается горизонтальной линией из трёх или более тильд. Никаких других символов, кроме пробельных, не должно быть на одной строке с линией:
~~~~~~~~~~~~~~~~~~~~~~~~~~
Copyright (C) 2012

This file is part of o42a.
~~~~~~~~~~~~~~~~~~~~~~~~~~

Для целей документирования предполагается использовать Markdown.

Переносы строк и символ подчёркивания

Предписания (statements) в o42a объединяются в предложения и могут разделяться различными знаками препинания: запятыми, точками, точками с запятыми, восклицательными или вопросительными знаками. Каждый из этих знаков имеет собственное назначение. Но об этом я расскажу позже. Важно, что знак препинания в конце строки не обязателен — тогда предполагается точка. Переносить выражения или предписания на следующую строку нужно явно, с помощью знака подчёркивания:
Sum = 
_left operand +
_right operand

Знак подчёркивания можно ставить в конце предыдущей или в начале следующей строки.

Также знак подчёркивания необходимо использовать для разделения имён, поскольку сами имена могут содержать пробелы:
Print error _nl ~~ Отправляет символ переноса строки "\n"
                ~~ в стандартный поток вывода ошибок (stderr из C).

Прочее

Десятичные числа:
1 234 567

Литералов для вещественных чисел или для шестнадцатеричной записи в языке нет. Однако это не проблема, поскольку есть фразы (подробнее о них позже):
float '3,141 592 653 59'

Строки «склеиваются» как в C:
"abc" "def" ~~ То же, что и:
"abcdef"

Экранирование обычное, с использованием обратной косой черты. Коды уникода (Unicode code points) всегда записываются в шестнадцатеричном виде и экранируются особым образом:
"\t \42a\ \" \' \\ \r\n"

Есть поддержка многострочного текста. Экранирование в многострочном тексте не работает:
""""""
Много
строк
текста
""""""

Объекты


Объект — это основная сущность o42a.

Объект создаётся путём наследования от другого объекта. Это единственный способ создания объектов.

Все объекты прямо или опосредованно унаследованы от объекта Void — единственного объекта, который не унаследован ни от кого.

Поля и наследование

У объекта могут быть поля. Поле — это вложенный именованный объект.

Вот пример объявления поля:
Object := void (
  Field := "Value" ~~ `Field` - это поле объекта `Object` унаследованное от `String`.
)

По умолчанию у полей публичная область видимости. Но можно объявлять их внутренними (private) и защищёнными (protected).

Любое выражение в o42a либо обращаются к существующему объекту, либо создаёт новый. Других выражений не бывает. Таким образом, абсолютно любое выражение в o42a — это ссылка на объект. Строковый литерал — это ссылка на объект, унаследованный от стандартного объекта String. Число — соответственно от Integer.

Обратиться к полю объекта можно через двоеточие:
Object: field

Любой объект может быть унаследован. При наследовании объекта также наследуются и все его поля. При этом поля можно перегружать:
Derived object := object ( ~~ `Derived object` унаследован от `Object`.
  Field = "New value" ~~ Поле `Field` перегружено.
)

Для перегрузки поля используется знак = вместо :=.

Однако в наследующем объекте можно объявить поле с точно таким же именем, что и в наследуемом:
Another object := object (
  Field := 123 ~~ Это новое поле с тем же именем `Field`.
)

При этом в новом объекте будет два поля с одинаковыми именами. Обратиться к ним можно следующим образом:
Another object: field                 ~~ 123
Another object: field @object         ~~ "Value"
Another object: field @another object ~~ 123

Прототипы и абстрактные поля

Как не трудно видеть, объекты заменяют собой классы. Действительно, зачем вообще нужны классы, если объекты обладают полной информацией о собственной структуре? Остаётся только одно применение: когда нужно задать (абстрактный) программный интерфейс и несколько разных его реализаций.

Для этих целей можно использовать прототипы:
Interface :=> void (
  ~~~
  Прототип. Объявляется с помощью значка `:=>`.
  ~~~
  Name :=< string
  ~~~
  Абстрактное поле. Объявляется с помощью значка `:=<`.
  ~~~
)

Отличие прототипов от обычных объектов в том, что к их содержимому (полям например) нельзя обращаться. Следующий код приведёт к ошибке:
Interface: name ~~ ОШИБКА: `Interface` - это прототип.

Но прототип можно наследовать, как и любой другой объект. Это, по сути, единственное, для чего он нужен.

Кроме того, прототип может содержать абстрактные поля. Такие поля должны быть перегружены при наследовании:
Implementation 1 := interface (
  Name = "Implementation 1"
  Implementation 1-specific field := 1
)
Implementation 2 := interface (
  Name = "Implementation 2"
  Implementation 2-specific field := 2
)

По своему назначению прототип — это обычный класс. В отличие от него, обычный объект в o42a представляет собой одновременно и класс, и его экземпляр.

Последнее должно быть знакомо программистам на Java. Это анонимные классы:
Runnable task = new Runnable() {
    @Override
    public void run() {
        System.err.println("Done!");
    }
}

Это выражение создаёт и анонимный класс, и его экземпляр. Отличие o42a в том, что созданные таким образом «классы» не обязательно анонимны.

Значения объектов

У каждого объекта есть значение. Тип этого значения наследуется от объекта — предка и не может быть изменён, если только это не void.

В o42a существует несколько типов значений. Каждый из них представлен стандартным объектом. Вот несколько простых типов:
  • Void — пустое значение, базовый тип для всех остальных.
  • Integer — 64-битное целое.
  • Float — 64-битное число с плавающей запятой.
  • String — строка символов уникода.

Существуют и более сложные типы: например ряды и массивы, связи и переменные. Система типов будет расширяться со временем, по мере надобности.

Значение объекта — это не обязательно константа. Оно вычисляется с помощью алгоритма, заданного определением. Определение значения — это набор предписаний (statements) в теле объекта. Оно может быть достаточно сложным: с условиями, циклами и всем, что угодно. Но собственно значение объекта задаётся предписанием (возврата) значения вида:
= value

Следующие объявления равнозначны:
Value := 5
~~ То же самое, что и:
Value := integer (= 5)

Определение значения наследуется и может быть перегружено.

Вот пример определения суммы двух чисел:
Sum :=> integer (
  Left operand :=< integer
  Right operand :=< integer
  = Left operand + right operand
)

Обратите внимание, что одно и то же определение может приводить к разным значениям:
Sum (Left operand =  1. Right operand =  2) ~~ 3
Sum (Left operand = -1. Right opernad = 10) ~~ 9

Адаптеры и образцы

При создании объекта, помимо предка можно указать один или несколько образцов, по которым объект будет создан:
Object := ancestor & sample 1 & sample 2 (~~ Определение ~~)

При этом поля и определения образцов будут унаследованы новым объектом, а сам объект станет совместимым с образцами (в смысле принципа подстановки Барбары Лисков). Возможные конфликты наследования при этом будут разрешены в соответствии с определёнными правилами.

Да, это множественное наследование. Но всегда ли уместно его применять? На практике наследование (в том числе множественное) применяется в одном из трёх случаев:
  1. Чтобы указать, что объект — это вариант унаследованного им объекта, то есть вместо частицы «это». Например «Арбуз — это ягода».
  2. Чтобы придать объекту некие свойства, — для этого в некоторый языках используются «примеси» (traits, mixins).
  3. Чтобы добавить объекту дополнительный программный интерфейс или привести к другому типу — в этом случае лучше использовать композицию.

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

Адаптер — это поле объекта, идентификатором которого является не имя, а другой объект:
Foo := void (
  Value := 123
  @String := "Foo=" + value ~~ `String` после знака `@` означает
                            ~~ ссылку на объект, а не имя поля.
)

Объект-адаптер всегда наследует свой объект-идентификатор. При приведении типов o42a сначала проверяет не унаследован ли объект от нужного, а затем пытается использовать адаптер к нему. Так что вот этот код:
Print [foo] nl

Напечатает Foo=123, несмотря на то, что параметром Print должна быть строка, а объект Foo от String не унаследован. В качестве параметра будет передан соответствующий адаптер.

Стандартные типы значений приводятся к строке и другим типам именно с помощью адаптеров.

К адаптеру можно обращаться и напрямую:
Foo @@string

А также можно обращаться к полям самого адаптера:
Foo: length @string ~~ 7

Обратите внимание, что синтаксис обращения к полям адаптера такой же, как для обращения к полям самого объекта. Разве что указание на источник поля (@string в данном случае) — обязательно.

Адаптеры также применяются и в других случаях. Например, для обозначения главного объекта приложения:
Use namespace 'Console' ~~ `Print` и `Main` объявлены в модуле `Console`.

@Main := * { ~~ Звёздочка используется, чтобы не писать `Main` второй раз.
  Print "Hello, World!" nl
}

Выполнение приложения будет состоять в обращении к адаптеру @Main, который и напечатает знаменитый текст.

Применений можно придумать множество. Можно, например, использовать адаптеры для подсчёта хеш-функции произвольного объекта. Достаточно определить адаптер @Hash code.

Адаптеры позволяют добавлять нужную функциональность любому объекту. Это избавляет от необходимости иметь в базовом объекте Void какие-либо поля. Кроме того, это типобезопасно, в отличие от аннотированных или особым образом поименованных методов вида __str__.

Обобщённое программирование


В o42a нет привычных шаблонов (templates) или обобщений (generics) с характерными им параметрами-типами. Каждый объект в o42a уже является обобщением, параметризованным включающим его объектом и соседними полями.

Дело в том, что ссылка на какой-либо объект, как правило, не статична. Это значит, что если унаследовать объект, такую ссылку содержащий, то наследующий объект по той же ссылке может получить другой объект.

Вот пример:
Base := void (
  A := void (
    F := 123     ~~ `Base: a` содержит поле `F'.
  )
  B := a (       ~~ `Base: b` унаследован от `Base: a`.
    А = 456
  )
)

Object := base ( ~~ Наследуем `Base`.
  A = * (        ~~ Перегружаем `A`.
    G := f * 10  ~~ Добавляем поле `A: g`.
  )
)

Object: a: g     ~~ 1230
Object: b: g     ~~ 4560

Обратите внимание, что поле Object: a: g было определено уже после Base: b. Тем не менее, выражение Object: b: g абсолютно корректно.

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

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

Обобщая вышесказанное


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

И, конечно же, я опустил множество деталей. Это материал для будущих статей.

Однако, целью данной статьи было познакомить вас с замыслом и его воплощением. Надеюсь, я всё понятно объяснил.

Итак, основная и единственная смысловая единица языка — это объект, который способен заменить собой очень многое:
  • Объект — это пространство имён
    Тут особо нечего комментировать: поля объекта — это символы в этом пространстве.
  • Объект — это класс
    Тоже описано выше. Классы не нужны, если наследовать непосредственно объекты.
  • Объект — это функция (и метод заодно)
    Аргументы такой функции — это поля объекта, а результат — это его значение.
    Наследование объекта — это вызов функции, перегрузка полей — это подстановка параметров (каррирование).
    Но объект может больше. Ведь его можно унаследовать снова, а поля — снова перегрузить (пере… каррировать?).
  • Объект — это обобщение
    Да, своеобразное. Однако возможностей даже больше.
    Объект-обобщение обладает тем же преимуществом перед традиционными обобщениями или шаблонами, что и объект-функция перед обычными функциями: поля-параметры можно перегружать снова и снова. Попробуйте-ка в обобщении Java или в экземпляре шаблона C++ заменить уже подставленный параметр-тип. А иногда очень хочется, чтобы не плодить лишние абстрактные классы.

Избыточность и нормализация


За всё нужно платить. И цена подхода «всё есть объект» высока.

Простейшее выражение
a + b
, где a и b — это целые числа, которые просто нужно сложить, на поверку оказывается нагромождением объектов:
Integers: add (
  Left operand = a
  Right operand = b
)
, которые нужно сконструировать (унаследовать, перегрузив поля), только для того, чтобы запросить их значения.

Впрочем, ничего неожиданного в этом нет. Сразу было ясно, что несоответствие между машинным представлением и человеческим пониманием приведёт к серьёзной избыточности этого самого машинного представления. Если, конечно, реализовывать его «в лоб». Но так делать не надо.

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

Техники нормализации довольно нетривиальны. И реализация этих техник находится в начальной стадии. Однако в таких простых случаях всё работает. Это можно проверить, скомпилировав, например, набор тестов o42a с включенной и отключенной нормализацией (o42ac -normalize=0). Размеры исполнимых файлов будут различаться вчетверо.

А вот так выглядит программа «Hello, World!» в LLVM IR:
hello_world.ll
; ModuleID = 'hello_world'
target datalayout = "E-p:64:64:64-S0-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f16:16:16-f32:32:32-f64:64:64-f128:128:128-v64:64:64-v128:128:128-a0:0:64"
target triple = "x86_64-pc-linux-gnu"

%o42a_val_t = type { i32, i32, i64 }

@CONST.STRING.1 = private constant %o42a_val_t { i32 6145, i32 13, i64 ptrtoint ([13 x i8]* @DATA.STRING.0 to i64) }
@CONST.STRING.2 = private constant %o42a_val_t { i32 1, i32 1, i64 10 }
@DATA.STRING.0 = private constant [13 x i8] c"Hello, World!"

define i32 @main(i32, i8*) nounwind {
main:
  call void @o42a_init() nounwind
  call void @o42a_io_print_str(%o42a_val_t* @CONST.STRING.1) nounwind
  call void @o42a_io_print_str(%o42a_val_t* @CONST.STRING.2) nounwind
  ret i32 0
}

declare void @o42a_init()

declare void @o42a_io_print_str(%o42a_val_t*)

Согласитесь, лишнего тут почти нет. И ни одной сложной структуры вроде объекта. А ведь в программе задействовано несколько.

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

Суть нормализации в том, что компилятор, вместо того, чтобы генерировать исполнимый код в соответствии с конструкциями в исходном, старается выяснить: как используется тот или иной объект в программе и, в соответствии с этим знанием, сначала упростить универсальный «объект» до минимально избыточной исполняющей сущности (константы, блока исполнимого кода, функции или указателя на одну из нескольких функций, структуры...) и сгенерировать исполнимый код уже для неё.

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

Но помимо нормализации реализовано ещё кое-что:
  • Во время компиляции вычисляется всё, что только можно. И это никакая не оптимизация, а основная функциональность. Без неё язык пришлось бы делать интерпретируемым.
  • Из программы выкидывается всё, что ей не используется: например, ненужные объекты, а также неиспользуемые поля в нужных объектах. Эта функциональность вполне ожидаема, так что к нормализации я её не отношу.

Состояние и перспективы


Проект o42a реализуется силами одного человека на условиях полного рабочего дня уже на протяжении трёх с половиной лет. Так что вы должны понимать, что отношусь я к этой работе серьёзно.

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

Разработка проекта находится на стадии реализации прототипа. Компилятор написан на Java и использует LLVM для генерации исполнимого кода. Исходные коды доступны под лицензией GPLv3+ (библиотеки времени исполнения — под LGPLv3+). У проекта есть сайт с документацией на довольно скверном английском языке, а также, пока ещё пустой, форум (прямые ссылки не привожу по понятным хабрапричинам).

Текущая версия o42a-0.2.4 содержит более 130 тысяч строк исходного кода. Компилятор всё ещё довольно хрупок, а библиотеки практически отсутствуют. Целевая платформа — GNU/Linux x86_64, на других тестирование не проводилось.

В ближайшие месяцы я планирую реализовать библиотеку коллекций, а также библиотеку ввода-вывода. Заодно отлажу компилятор, разрешу оставшиеся вопросы по самому языку, а также напишу примеров с Rosetta Code. Предполагается, что версией 0.3.0 уже можно будет как-то пользоваться.

С дальнейшими перспективами всё довольно туманно. Вначале я не предполагал, что разработка займёт столько времени, зато сейчас хорошо себе представляю объём работ. Для одного человека это много, а сбережения, на которые я жил в последние годы, истощились. Так что если разработку не возьмёт «под крыло» серьёзная контора, то она замедлится, поскольку мне придётся возвратиться во фриланс, на родной oDesk. Просить подаяний (donations) я пока не планирую. Как-то несерьёзно это: денег приличных не соберёшь, а обязанным при этом будешь.

Если у кого есть серьёзное желание заняться столь амбициозным проектом — обращайтесь. Я хоть и простой программист, но идей монетизации этого монстра могу придумать. Только нужно учитывать, что это не веб-два-нуль-цена-10к-хочу-мильён-стартап. Это серьёзные, долгосрочные вложения, в том числе в нешуточный маркетинг.

Если есть желание безвозмездно поработать во благо Open Source вообще и проекта o42a в частности, то у меня найдутся задачки, в том числе задача не связанная напрямую с o42a и полезная в качестве самостоятельного проекта (нужная библиотека на чистом C).

Если же у вас есть мысли о том, как можно профинансировать разработку, то будьте добры, поделитесь.
Tags:
Hubs:
+1
Comments 93
Comments Comments 93

Articles