Главное меню
Мы солидарны с Украиной. Узнайте здесь, как можно поддержать Украину.

ГриньСкрипт и нативный код

Автор Алексей Гринь, января 5, 2015, 21:19

0 Пользователи и 1 гость просматривают эту тему.

Алексей Гринь

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

Сначала опишу, что же есть замыкания.
Гринь-замыкания делятся на две категории: интерфейс и реализации.

Что есть замыкания

Интерфейс замыкания, т.н. «метод-класс» (то, что в C# называется Delegate) — это самый обычный абстрактный класс, у которого автоматически генерируется один виртуальный метод — "invoke", у которого сигнатура зависит от сигнатуры самого метод-класса.

Реализация замыкания — это автоматически сгенерированный ad hoc дериват от абстрактного класса, который реализует метод invoke (тело замыкания).

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

Таким образом, после того как замыкание создано, возвращённое значение — это по сути самый обычный указатель на самый обычный Гринь-объект. Тело замыкания — это переопределённый метод invoke, контекст замыкания — это внутреннее поле ClosureEnv внутри объекта. То есть всё, что нам нужно, чтобы запустить замыкание — это найти его указатель и вызвать метод invoke.

Thunking

Если же брать нативный код, то в C/x86 callback'и реализованы иначе. C-коллбэк это просто указатель на код. Отсутствует возможность передавать контекст данных внутрь C-callback'а, если сам C-callback не позволяет этого. Что же делать, как же трансформировать Гринь-замыкание в C-callback? Ведь Гринь-замыкание — это указатель на данные, C-callback — это указатель на код.

Делается это, как можно догадаться, с помощью thunks (как по-русски это вообще?) — динамически генерируемых небольших кусочков вспомогательного кода. Допустим, если взять сигнатуру int(*)(int, int), то нативный код ожидает указатель на код, который pop'ит 2 аргумента с нативного стека и возвращает результат через eax. Метод invoke у Гринь-замыканий же имеет внутренне другую сигнатуру: int(*)(void*, int, int), где первый указатель есть указатель this.

Для каждого замыкания генерируется свой thunk: он попросту push'ит все аргументы на стек ещё раз (те аргументы, что заpush'ил нативный код, ожидая обычный C-callback), а затем — самое главное — push'ит указатель this на наше замыкание и дальше производит вызов самого тела Гринь-замыкания. Вот как это примерно выглядит для указанной выше сигнатуры (+ cdecl):

push ebp
mov ebp, esp
mov eax, [ebp+0000000C] <-- аргумент номер 2
push eax
mov eax, [ebp+00000008] <-- аргумент номер 1
push eax
mov eax, 0134A670 <-- добавленный указатель this (наше замыкание)
push eax
mov eax, 0138CE1B <-- указатель на тело замыкания
call eax
add esp, 0000000C
leave
ret

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

Сгенерированный thunk хранится в новом внутреннем поле замыкания под названием m_codeOffset. Сборщик мусора был немного изменён, теперь он распознаёт замыкания и проверяет m_codeOffset, чтобы удалить thunk, когда удаляется само замыкание.
Это вводит известную для C# проблему ранней деаллокации thunk'ов, когда нативный код, возможно, ещё продолжает пользоваться thunk'ом, что приводит, ясным образом, к segfault'ам. Для того, чтобы это не произошло, нужно удостовериться, что ссылка на делегат трасируется от GC-корней. Или можно просто вызвать метод GC::addRoot и не париться.

Чтобы создать thunk из-под Гринь-скрипта, создан новый метод Marshal::codeOffset. Его название ортогонально методу Marshal::dataOffset, который извлекает указатель на непосредственные данные объекта (минуя vtable или count у массивов).
肏! Τίς πέπορδε;

Wolliger Mensch

«Вот интересно, каких лингвистических жемчуг можно найти в море отодвинутых книг», Ян Гавлиш.
«Впредь прошу помнить, что придумал игру не для любых ассоциаций, а для семантически оправданных. Например, чтó это такое: ,,рулетке" — ,,выпечке"?? Тем более, что сей ляпсус я сам совершил...», Марбол
«Ветхий Завет написан на иврите и частично на армейском», Vesle Anne
«МЛ(ять)КО ... ПЛ(ять)NЪ», Тася
«Вот откроет этот спойлер, например, Марго, ничего не подозревая, а потом будут по всему форуму блюющие смайлики...», Авал
«Томан приличный мужчина. Правда по патриархальным меркам слегка голодранец», Vesle Anne
«Возможен ли фонетический переход "ж" в "п с придыханием"», forest


Wolliger Mensch

«Вот интересно, каких лингвистических жемчуг можно найти в море отодвинутых книг», Ян Гавлиш.
«Впредь прошу помнить, что придумал игру не для любых ассоциаций, а для семантически оправданных. Например, чтó это такое: ,,рулетке" — ,,выпечке"?? Тем более, что сей ляпсус я сам совершил...», Марбол
«Ветхий Завет написан на иврите и частично на армейском», Vesle Anne
«МЛ(ять)КО ... ПЛ(ять)NЪ», Тася
«Вот откроет этот спойлер, например, Марго, ничего не подозревая, а потом будут по всему форуму блюющие смайлики...», Авал
«Томан приличный мужчина. Правда по патриархальным меркам слегка голодранец», Vesle Anne
«Возможен ли фонетический переход "ж" в "п с придыханием"», forest

Алексей Гринь

https://en.wikipedia.org/wiki/Closure_(computer_programming)

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

По теме — я могу рассказать ещё множество изумительных историй по поводу того, как ГриньСкрипт общается с нативным кодом.
肏! Τίς πέπορδε;

Wolliger Mensch

Цитата: Алексей Гринь от января  5, 2015, 21:35
Я, в принципе, не люблю русскоязычную терминологию, потому что это клоунада, как вы можете сами убедиться. Гринь-closure уже звучит не так глупо.

Тоже не очень. На клозет похоже. :3tfu:
«Вот интересно, каких лингвистических жемчуг можно найти в море отодвинутых книг», Ян Гавлиш.
«Впредь прошу помнить, что придумал игру не для любых ассоциаций, а для семантически оправданных. Например, чтó это такое: ,,рулетке" — ,,выпечке"?? Тем более, что сей ляпсус я сам совершил...», Марбол
«Ветхий Завет написан на иврите и частично на армейском», Vesle Anne
«МЛ(ять)КО ... ПЛ(ять)NЪ», Тася
«Вот откроет этот спойлер, например, Марго, ничего не подозревая, а потом будут по всему форуму блюющие смайлики...», Авал
«Томан приличный мужчина. Правда по патриархальным меркам слегка голодранец», Vesle Anne
«Возможен ли фонетический переход "ж" в "п с придыханием"», forest

Алексей Гринь

Добавил поддержку STDCALL (чего требует Windows API) при конвертации Гринь-замыканий в C-callback'и. Пришлось изменить пару структур опять. Дело в том, что calling convention у меня определяется атрибутами к методу (как в P/Invoke). Когда определяется класс делегата, то все атрибуты класса автоматически переходят на атрибут автоматически сгенерированного метода invoke:

Цитировать[callConv=stdcall]
method class (ThreadProc arg: intptr): int;

есть на самом деле синтаксический сахар к:
Цитировать[callConv=stdcall]
class ThreadProc {
[callConv=stdcall]
method (invoke  arg: intptr): int;
}

Замыкания есть ad-hoc дериваты базового класса, с переопределённым invoke. О чём я при дизайне системы не подумал, так это о том, что атрибуты переопределённых методов должны наследовать атрибуты базовых методов. Иначе у замыкания переопределённый invoke не будет иметь атрибут "stdcall", что чревато при кодогенерации.

Что касается thunk'ов для STDCALL, всё отличие от стандартного CDECL заключается в отсутствии "add esp, X".

И вот что я подумал ещё. Например, если взять CreateThread из WinAPI.  Мой скриптовый язык имеет «доменную» архитектуру, то есть вся экосистема делится на изолированные потоки, которые приравниваются к изолированным экземплярам виртуальной машины (для более безопасной реализации многопоточности). Однако же, если нативный код типа CreateThread берёт Гринь-замыкание и создаёт новый поток, то Гринь-код внутри нового потока ВНЕЗАПНО окажется вне своего родного домена, где он был создан. Это чревато вылетами, так как Гринь-код предполагает наличие VM-структур, привязанных к текущему потоку (а их в этом случае нет).

Вопрос: гарантировать ли инъекцию нового Гринь-домена в девственный поток в таких случаях? Мы можем сделать проверку и инъекцию прямо внутри thunk'ов. Правда, я пока не знаю, как потом удалить такой домен. Не знаю, как поймать событие «поток N завершён»

Это было бы интересно реализовать, но это не приоритет.
肏! Τίς πέπορδε;

Алексей Гринь

О, я думаю, инъецировать Гринь-домен в девственное лоно Windows-потоков я не буду. Вместо этого я буду проверять внутри thunk'ов — внутри родного ли домена мы находимся и каким-то образом делать "fail fast" если это не так, т.е. фэйлить по-тихому, пока чего плохого не произошло. Но непонятно как это делать. Нативный поток может быть критичным. То есть завершать его не следовало бы. Однако, в любом случае, самый первый аборт на стороне Гринь-скрипта уронит поток, так как в дественном С-потоке будут отсутствовать хэндлеры для sjlj-исключений, на которых основаны аборты. Отсутствие хендлера приведёт к падению всего процесса, в том числе к падению всех якобы изолированных друг от друга доменов, что совершенно не желательно. Поэтому было бы умно убить текущий поток заранее — тогда мы хотя бы сохраняем жизнь другим независимым доменам в текущем процессе, пусть и засчёт какого-то левого убитого C-потока.

С другой стороны, возможность неза'sandbox'енного домена вызывать нативные методы сама по себе небезопасная фича, что, казалось бы, говорит нам о том, что заморачиваться не стоит. Однако у нативных функции интерфейсы явно указаны в хедерах, что предсказуемо и в какой-то мере безопасно. Если же брать callback'и, то ты никогда не знаешь, где этот callback может очутиться, потому что реализация скрыта. Поэтому, я думаю, всё-таки, проверка на наличие привязанного к текущему потоку Гринь-домена оправданна, несмотря на что, что работа с нативным кодом сама по себе небезопасная.
肏! Τίς πέπορδε;

Алексей Гринь

О нет, новая идея. Убивать рандомный поток и игнорировать это нельзя! Вместо этого, если определено, что замыкание вызвано вне своего домена, мы будем показывать MessageBox с текстом типа  (чисто для debug'инга.) «Замыкание вызвано native-кодом вне своего родного домена. Работа процесса с данного момента может быть нестабильной. Продолжить?»

Вот так вот ваще гениально. Я молодец.
肏! Τίς πέπορδε;

Алексей Гринь

Подсчитал количество абстракций и реализации при обёртывании событий моего С++-фреймворка Гринь-скриптом:

1) C++ код вызывает Event::Fire(..) который проходит под внутреннему массиву EventHandler's и вызывает их Invoke(..), где аргументы зависят от C++-шаблона;

2) EventHandler в этом случае реализован как частный случай EventHandlerWrapper — обёртка вокруг C-callback'а EVENTHANDLER для межмодульного взаимодействия;

3) C-callback на самом деле есть сгенерированный C-to-ГриньСкрипт transition thunk, который inject'ит "this" и передаёт управление в скомпилированный машинный код уже на стороне ГриньСкрипта;

4) Функция, где мы теперь оказываемся, есть т.н. Virtual Call Helper, смысл которой есть проверять "this" на нуль в мягком режиме, разыменовывать vtable по заhardcod'енному индексу и переходить по полученному адресу;

5) Мы наконец-то находимся в обработчике события на стороне ГриньСкрипт.
肏! Τίς πέπορδε;

Rachtyrgin

Цитата: Алексей Гринь от января  5, 2015, 21:19
Самое главная фишка замыканий — возможность захвата переменных в собственный контекст, да так, что, например, локальные переменные функции могут «жить» даже после того, как сама функция завершилась.

Это что - демонстративное "фе" принципам ООП? Ваши замыкания и до private переменных добираться будут?
Всякому остановленному фашисту для захвата его в плен можешь еще крикнуть:
«Хэндэ хох!» (Руки вверх!)
«Вафи хинлеги!» (Бросай оружие!)
«Абгезэсен!» (Слезай! — С машины, с лошади, с повозки.)
Если фашист не сразу исполняет твое приказание, крикни грознее и добавь:
«Бай флухтфэрзух вирт гэшози!» (Побежишь — буду стрелять!)
А. Афанасьев. В помощь партизану. Москва, 1942 г.

Алексей Гринь

Цитата: Rachtyrgin от января 10, 2015, 17:41
Это что - демонстративное "фе" принципам ООП? Ваши замыкания и до private переменных добираться будут?
Почему демонстративное «фе»? Инкапсуляция никак не нарушается. Если замыкание создано внутри метода, у которого есть доступ к переменной (приватной), то почему нет? Метод объекта (который имеет доступ к приватным данным) неявно разрешил замыканию доступ к переменным в текущем контексте самим фактом создания замыкания. Более того, к захваченным переменным, на которые ссылается замыкание, нет доступа извне замыкания. Замыкание это тот же самый обычный код, что и код метода, и правила доступа те же; разница только в том, что на замыкание как на код можно взять ссылку. Посторонний код, который пользуется замыканием, ничего не знает о том, какие переменные оно захватило. Он просто вызывает его как функцию. Это и есть инкапсуляция.

В ГриньСкрипте с инкапсуляцией всё в порядке, она у нас, наоборот, более форсированная, чем в некоторых языках: нет не-приватных полей вообще. Только владелец поля может к нему обращаться. У нас даже не предусмотрено синтаксиса для обращения к полям чужих объектов. Чтобы обратиться к полю в чужом объекте, нужно или вручную написать публичные getter/setter-методы, или использовать генеративный синтаксический сахар "property", который автоматически создаёт их.
肏! Τίς πέπορδε;

Rachtyrgin

А, замыкание внутри метода? Тогда ладно. Из начала вашего топика не уловил. А у вас язык предполагается чисто скриптовый, или все-таки будет промежуточная компиляция в бинарный код в духе Java?
Всякому остановленному фашисту для захвата его в плен можешь еще крикнуть:
«Хэндэ хох!» (Руки вверх!)
«Вафи хинлеги!» (Бросай оружие!)
«Абгезэсен!» (Слезай! — С машины, с лошади, с повозки.)
Если фашист не сразу исполняет твое приказание, крикни грознее и добавь:
«Бай флухтфэрзух вирт гэшози!» (Побежишь — буду стрелять!)
А. Афанасьев. В помощь партизану. Москва, 1942 г.

Алексей Гринь

Цитата: Rachtyrgin от января 10, 2015, 18:16
А у вас язык предполагается чисто скриптовый, или все-таки будет промежуточная компиляция в бинарный код в духе Java?
Скриптовый в том плане, что вручную не нужно что-то компилировать, линковать и т.д. — просто запустил, и всё. Однако в конечном счёте перед запуском он компилируется в бинарный код и имеет скорость неоптимизированного Си.

Компилирую я это весьма экзотическим способом — код генерируется в памяти в промежуточный код на С, который затем компилируется высокоскоростным TCC, опять же, в памяти (TCC не генерирует AST и почти ничего не проверяет, отсутствует basic blocks и нет аллокации регистров — он генерирует код на лету по мере парсинга; так, кстати, делали ранние версии Google Chrome). Можно сказать, это у нас это и есть такой промежуточный высокоуровневый байткод.

Благодаря тому, что бэкенд у меня — Си, язык поддерживает inline C, а также inline asm внутри inline C.

Я пока сильно бенчмарками не занимался, и сужу по дебаг-билду, но, как ни странно, парсинг и анализ Гринь-скрипта занимает больше времени, чем компиляция Си. Так что TCC хорошо оптимизирован и его использование вполне оправданно, несмотря на кажущуюся громоздкость идеи.

Так как язык строго типизированный, перед запуском приходится анализировать полностью весь исходный код, разрешать все ссылки и т.д., что довольно трудозатратно, по сравнению с динамическими языками, и меня волнует то, что по мере роста скрипта, растёт задержка при запуске (сейчас в пределе 100 мс в моих тестах, но это дебаг-билд). Поэтому в будущем думаю как-то кэшировать скомпилированный результат на диске.
肏! Τίς πέπορδε;

Rachtyrgin

Скорость Си? Впечатлен. А как насчет кроссплатформенности? Или только Windows?
Всякому остановленному фашисту для захвата его в плен можешь еще крикнуть:
«Хэндэ хох!» (Руки вверх!)
«Вафи хинлеги!» (Бросай оружие!)
«Абгезэсен!» (Слезай! — С машины, с лошади, с повозки.)
Если фашист не сразу исполняет твое приказание, крикни грознее и добавь:
«Бай флухтфэрзух вирт гэшози!» (Побежишь — буду стрелять!)
А. Афанасьев. В помощь партизану. Москва, 1942 г.

Алексей Гринь

Цитата: Rachtyrgin от января 10, 2015, 18:54
Скорость Си? Впечатлен. А как насчет кроссплатформенности? Или только Windows?
Не тестил, но там Windows-моментов не так много. Портировать на Линукс можно за пару вечеров.

Бо́льшая проблема в том, что только x86 в данный момент. Я бы попробовал x64, да нет возможности, времени и сил.

В любом случае, есть Wine и WOW64.
肏! Τίς πέπορδε;

Алексей Гринь

Я вот тут описал базовую архитектуру: Доменная инфраструктура в ГриньСкрипте
Там полотна текста, но, может быть, кому-то задумка будет интересна.
肏! Τίς πέπορδε;

Rachtyrgin

Цитата: Алексей Гринь от января 10, 2015, 19:03

Бо́льшая проблема в том, что только x86 в данный момент. Я бы попробовал x64, да нет возможности, времени и сил.

В любом случае, есть Wine и WOW64.

Понимаю. Я одну свою программулину (Java + FireBird) пытался вести одновременно на x86 и x64, потом плюнул и ушел на x64 полностью. А "полотна " вашего текста я почитаю. Любопытно.
Всякому остановленному фашисту для захвата его в плен можешь еще крикнуть:
«Хэндэ хох!» (Руки вверх!)
«Вафи хинлеги!» (Бросай оружие!)
«Абгезэсен!» (Слезай! — С машины, с лошади, с повозки.)
Если фашист не сразу исполняет твое приказание, крикни грознее и добавь:
«Бай флухтфэрзух вирт гэшози!» (Побежишь — буду стрелять!)
А. Афанасьев. В помощь партизану. Москва, 1942 г.

Bhudh

Цитата: Алексей Гринь от января 10, 2015, 18:48Благодаря тому, что бэкенд у меня — Си, язык поддерживает inline C, а также inline asm внутри inline C.

Когда-то обсуждали старую статью: «Наш ответ Чемберлену»
Вот из неё цитата:
Цитата: Следующим хитом станет, сдается мне, язык, который будет написан "для того, чтобы избавить автора и его друзей от программирования на...". (На чем-то уровня С/С++, Pascal, Delphi). При этом, как хорошие среды разработки содержат возможность ассемблерных вставок, так и новая среда будет содержать возможность вставок на заменяемом языке, да еще и про ASM не забудет.

Понятия нового языка должны представлять собой уровень уже более высокий, чем даже Delphi. Потому что, какой смысл изобретать еще один Delphi, еще один SQL, еще один С-клон?
Пиши, что думаешь, но думай, что пишешь.
MONEŌ ERGŌ MANEŌ.
Waheeba dokin ʔebi naha.
«каждый пост в интернете имеет коэффициент бреда» © Невский чукчо

Тайльнемер

Цитата: Алексей Гринь от января 10, 2015, 18:48
Компилирую я это весьма экзотическим способом — код генерируется в памяти в промежуточный код на С, который затем компилируется высокоскоростным TCC, опять же, в памяти.
...и меня волнует то, что по мере роста скрипта, растёт задержка при запуске (сейчас в пределе 100 мс в моих тестах, но это дебаг-билд). Поэтому в будущем думаю как-то кэшировать скомпилированный результат на диске.
А если поддерживается только одна архитектура, почему бы сразу не компилировать в файл?

Алексей Гринь

Цитата: Тайльнемер от января 11, 2015, 04:41
А если поддерживается только одна архитектура, почему бы сразу не компилировать в файл?
Ну это типа скрипт же. Если и компилировать в файл, то только в качестве кэша, чтобы экономить время запуска при следующих запусках, если файл не менялся.
Мне так проще, пока что. Много вспомогательных функций, которые линкятся динамически, код же постоянно бегает:

бинарный ГриньСкрипт-код >> runtime helpers >> внутренние кишки (классы Class, Method, Domain в C++) >> и обратно

и оно как-то попроще если линковать всё это в динамике, в памяти. TCC немного баговатая штука, при определённых сочетаниях вылетает и работает не как надо, я пока что это не трогаю, потому что don't fix it, if ain't broke.

Другая вещь заключается в том, что многопоточность у меня заключается в том, чтобы создавать отдельный изолированный экземпляр виртуальной машины (т.н. домен, по аналогии с C#'овским AppDomain), и при запуске этот новый экземпляр компилирует собственный код, собственную версию всего. И фишка в том, что можно создавать домены из строк, как eval в JavaScript'ах (однако у меня есть встроенная система безопасности, так что если новый домен помечен как sandboxed, первая же попытка попытаться в нём что-то сделать опасное убьёт домен с ошибкой "Code access denied"). Так вот, тут неразбериха полная будет, если каждый запускаемый домен (т.е. поток) будет генерировать собственный исполняемый файл на диске.

Задержка при запуске растёт не из-за компиляции как таковой, а из-за того, что строго типизированному языку нужно проанализировать всю иерархию классов, прежде чем что-то делать: чтобы код в одном файле мог правильно ресолвить ссылку в другом файле, чтобы layout vtabl'ов сделать, чтобы провести Hierarchy Analysis и схлопнуть одинокие виртуальные методы в статические вызовы, чтобы всё двадцать раз проверить (например, ECall-вызовы, то есть вызовы к внешним нативным библиотекам, разрешены только в unsafe-контекстах в fulltrust-доменах) и т.д. Поэтому как вариант я думал о том, чтобы кэшировать такую runtime-информацию, а не сам скомпилированный бинарный код.
肏! Τίς πέπορδε;

Алексей Гринь

Только что успешно проверил inline ASM внутри inline C. Можно из-под чистого ГриньСкрипта, не используя сторонних инструментов, обратиться к процессору напрямую и получить название модели. Вот что мне возвратил процессор:

Intel(R) Core(TM)2 Quad  CPU   Q8200  @ 2.33GHz
肏! Τίς πέπορδε;

Bhudh

Хм. А в какой кодировке там записываются строки? ANSI?
Пиши, что думаешь, но думай, что пишешь.
MONEŌ ERGŌ MANEŌ.
Waheeba dokin ʔebi naha.
«каждый пост в интернете имеет коэффициент бреда» © Невский чукчо

Алексей Гринь

Цитата: Bhudh от января 11, 2015, 10:45
Хм. А в какой кодировке там записываются строки? ANSI?
Алгоритм такой: записываешь в eax по очереди значения 80000002H, 80000003H и 80000004H и каждый раз вызываешь инструкцию cpuid. Каждый раз он возвращает по четыре байта в eax, ebx, ecx и edx.

Эти байты просто аккумулируешь в буфере, затем добавляешь терминирующий нуль, вызываешь ГриньСкриптовское Marshal::utf8ToString и получаешь строку. Да, это обычный ANSI, он совпадает с UTF8 для английский букв, поэтому я использовал utf8ToString.

Как я проверил, это же значение Windows 7 пишет, когда смотришь свойства системы.
Это создаю сейчас примеры для первого публичного релиза, хотя наверное всем пофиг.
肏! Τίς πέπορδε;

Тайльнемер


Быстрый ответ

Обратите внимание: данное сообщение не будет отображаться, пока модератор не одобрит его.

Имя:
Имейл:
Проверка:
Оставьте это поле пустым:
Наберите символы, которые изображены на картинке
Прослушать / Запросить другое изображение

Наберите символы, которые изображены на картинке:

√36:
ALT+S — отправить
ALT+P — предварительный просмотр