
Интересный небольшой эксперимент по использованию Cи в качестве цели компиляции для получения портативности программы, ее оптимизации и функциональной совместимости. В ходе эксперимента мы также напишем саму программу, реализующую алгоритм Эвклида, выполним ее отладку и профилирование, а также попутно задействуем функцию «красивой» печати gdb.
Ниже показан процесс отладки программы Forth в KDevelop – графическом отладчике, не поддерживающем Forth:

Круто, не так ли? По умолчанию здесь есть только добавленное кем-то ранее выделение синтаксиса для файлов Forth. Остальные же возможности вроде выполнения в этом отладчике Forth, определения точек останова и просмотра состояния программы мы реализуем ниже сами.
Я покажу вам весь необходимый код. Делать нам придется не так много, поскольку большую часть всего мы получим в готовом виде, использовав Си в качестве промежуточного языка.
Применение высокоуровневого языка на роли промежуточного нельзя назвать необычным. Многие компиляторы нацелены на существующую высокоуровневую платформу, а не генерацию нативного кода – та же генерация байткода JVM или исходного кода JS. Почему? Потому что таким образом мы попутно получаем:
- портативность;
- оптимизацию;
- некоторую функциональную совместимость библиотек;
- некоторую функциональную совместимость инструментов (IDE, отладчиков и т.д.).
Среди языков, нацеленных на высокоуровневые платформы, есть весьма популярные: Scala и Clojure, чьи компиляторы ориентированы на JVM, CoffeeScript и Dart, которые уже компилируются в JavaScript. (Есть еще Java, код которого, как известно, GWT компилирует в JavaScript – хотя это все же отдельный случай).
Какие из подобных языков являются наиболее успешными? Без сомнений на сегодня ответом будет С++ и Objective-C – языки, чьи первые компиляторы генерировали код Си.
Я считаю, что Си является прекрасным промежуточным языком для компиляции. Он чрезвычайно портативен, быстро компилируется, хорошо оптимизируется, и при этом вы получаете функциональную совместимость с кучей возможностей.
Когда я хотел создать компилятор для интерпретируемого языка нашей собственной разработки, то изначально думал о генерации кода для VM, а не исходного языка. Я планировал получать LLVM IR, но потом подумал, а почему именно LLVM IR? В конце концов LLVM IR менее читаем, чем Си, менее стабилен, чем стандарт Си, и при этом менее портативен. Скорее всего, так всегда и будет.
Даже если LLVM выполняется на каждой аппаратной платформе, LLVM IR будет поддерживаться только инструментами LLVM, но уже не GNU или Visual Studio, к примеру. При этом генерация кода Си обеспечивает отличную поддержку и инструментов LLVM, и GNU и VS. Отладка сгенерированной LLVM IR программы в Visual Studio наверняка будет уступать аналогичному процессу для автоматически сгенерированного кода Си, скомпилированного через Visual Studio.
«Cи в качестве промежуточного языка» — это одна из тем, о которой я думал написать уже несколько лет. Мешало лишь то, что я хотел сделать это на примере, включая дополнительную работу, которая может потребоваться для лучшей поддержки отладки. Вот только пример, который бы вместился в компактный формат блога, никак придумать не получалось.
Пока однажды меня не осенило: Forth! Минималистский язык, в который я когда-то влюбился (статья на англ.), и который по-прежнему занимает место в моем сердце.
Итак, я создам компилятор из Forth в C. Данный проект впишется в один пост – по крайней мере в него войдет небольшой сегмент Forth – а этот язык достаточно отличен от Си, чтобы вызвать интерес. Так я хочу показать, что это не обязательно должен быть Си с расширениями вроде С++ или Objective-C. Это может быть нечто далекое от него, но вы все равно получите много пользы от инструментов Си, предоставляемых вашей платформой.
Что ж, хватит лишних слов, пора переходить к делу – реализации экспериментального компилятора из Forth в Cи. План будет таков:
- Определить небольшой сегмент Forth.
- Реализовать простой компилятор и среду выполнения.
- Отладить программу Forth с помощью gdb.
- Выполнить профилирование программы Forth при помощи KCachegrind.
- Вывести стек данных через функцию «красивой» печати gdb.
- Провести отладку в KDevelop, используя представленный стек данных.
Безопасный комплект Forth
Опасным было бы включить поддержку
CREATE/DOES>, COMPILE, POSTPONE или чего-то подобного. Мы этого делать не будем и задействуем лишь столько Forth, чтобы хватило для реализации Эвклидова алгоритма (gcd).Вот описание нашего сегмента Forth – если этот язык вы знаете, то можете его пропустить:
- В Forth используется стек данных.
- В этот стек передаются целые числа. Когда мы вводим
2 3, сначала передается2, а затем3. - Арифметические операторы извлекают из стека операнды и помещают туда результат. К примеру,
6 2 /извлекает6и2, помещая6/2=3. При этом2 3 =помещает0(ложно), потому что2не равно3. - Слова для управления стеком.
DUPповторяет вершину стека:2 DUPравнозначно.2 2Swapпроизводит обмен значений:2 3 SWAPравнозначно3 2.Tuckвыполняет подбор, т.е. помещает значение из вершины стека в его третью позицию:2 3 TUCKравнозначно3 2 3. Как вы уже можете себе представить, чем меньше таких слов, тем более читаемый получается код. - Новые слова определяются с помощью
: MYNAME …code… ;Если далее вы введетеMYNAME, то выполните код и при достижении точки с запятой вернетесь к моменту вызова. Никаких «аргументов функций» не объявляется – вместо этого код извлекает аргументы из стека и помещает туда результат. Предположим,: SQUARE DUP * ;определяет слово, возводящее значение в квадрат; теперь3 SQUAREравнозначно3 DUP *– извлекает из стека3и помещает туда9. - Циклы:
BEGIN … cond UNTILаналогичен{ … } while(!cond), иcondв нем извлекаетсяUNTIL.BEGIN … cond WHILE … REPEATаналогиченwhile(1) { … if(!cond) break; … }, иcondв нем извлекаетсяWHILE. - Вывод:
2 .выводит2.CRвыводит новую строку. - Forth нечувствителен к регистру*.
*Прим. пер.: существуют реализации Forth, чувствительные к регистру.
Вот и все – этого достаточно, чтобы реализовать
GCD и напугать (ну или поразить) ваших привыкших к инфиксам друзей.«Стратегия компиляции»
Стратегия, конечно, громко сказано, но все же наш компилятор, втиснутый в масштаб блога и имеющий соответствующие возможности, будет работать так:
- Стек – это массив
data.data– этоtypedefдляlong. - Каждое определяемое пользователем слово компилируется в функцию Си, которая получает
data*, указывающую на вершину стека (TOS), и возвращает новый указатель TOS. - Некоторые встроенные слова допускается использовать аналогично пользовательским:
s=WORD(s). - Так работают не все слова:
+не может статьs=+(s), потому что это не валидный Cи. Для таких слов мы выполняем простое преобразование.2становитсяPUSH(2),+становитсяOP2(+). Аналогичным образом слова потока управления переводятся вdo/while,ifиbreak. - Стек растет сверху вниз и уменьшается в обратном направлении: если
s[0]представляет TOS, значитs[1]будет элементом под ним и т.д. Командаs++уменьшает стек, извлекая TOS.
Вот и все. Например, функция
gcd отсюда:: gcd begin dup while tuck mod repeat drop ;…компилируется в код Си, приведенный ниже. Как видите, каждое слово преобразуется по-отдельности – у компилятора практически нет понятия «контекста» или «грамматики»:
data* gcd(data* s) { //: gcd
do { //begin
s=dup(s); //dup
if(!*s++) break; //while
s=tuck(s); //tuck
OP2(%); //mod
} while(1); //repeat
s=drop(s); //drop
return s; //;
}Вот весь исходный код Python для компилятора (
forth2c.py) – немного повторяющийся после всех объяснений:#!/usr/bin/python
import sys
# "special" слова – не способны генерировать s=WORD(s)
special = {
':':'data* ',
';':'return s; }',
'.':'PRINT();',
'begin':'do {',
'until':'} while(!*s++);',
'repeat':'} while(1);',
'while':'if(!*s++) break;',
}
# бинарные операторы
binops='+ - * / = < > mod'.split()
op2c={'=':'==','mod':'%'}
def forth2c(inf,out):
n = 0
for line in inf:
n += 1
# генерирует строковую информацию для инструментов Си (отладчиков и т.д.)
# - приятная опция Си дает нам:
print >> out,'n#line',\n,'"%s"'%infile
for token in line.lower().strip().split():
if token in special:
print >> out,special[token],
else:
try:
num = int(token)
print >> out, 'PUSH(%d);'%num,
except ValueError:
if token in binops:
print >> out,'OP2(%s);'%op2c.get(token,token),
else:
if defining:
print >> out,token+'(data* s) {',
else: # call
print >> out,'s=%s(s);'%token,
defining = token == ':'
out = open('forth_program.c','w')
print >> out, '#include "forth_runtime.h"'
for infile in sys.argv[1:]:
forth2c(open(infile),out)А вот «библиотека среды выполнения», если можно ее так назвать (
forth_runtime.h). Она представляет некую форму предполагаемого манипулирования стеком: s++, s--, s[0]=something.#include <stdio.h>
typedef long data;
#define PUSH(item) (--s, *s = (item))
#define OP2(op) (s[1] = s[1] op s[0], ++s)
#define PRINT() (printf("%ld ", s[0]), ++s)
#define cr(s) (printf("\n"), s)
#define drop(s) (s+1)
#define dup(s) (--s, s[0]=s[1], s)
#define tuck(s) (--s, s[0]=s[1], s[1]=s[2], s[2]=s[0], s)
#define MAX_DEPTH 4096 //произвольно
data stack[MAX_DEPTH];
data* forth_main(data*);
int main() { forth_main(stack+MAX_DEPTH); return 0; }Заметьте, что в нашем диалекте Forth есть слово для точки входа –
forth_main – которому Си-функция main передает указатель на пустой стек. В реальном Forth нет точки входа – он больше подобен скриптовому языку. Однако хак с forth_main для нашего примера вполне сработает.Вот и все! Весь диалект уместился в 60 строк. Самый короткий из всех написанных мной компиляторов, если не самый эффективный.
Тест
Протестируем наш
forth2c, используя несколько примеров исходного кода.countdown.4th (отсюда)
: COUNTDOWN
BEGIN
DUP .
1 -
DUP 0 =
UNTIL
DROP
;gcd.4th (многострочная версия – с целью упрощения дальнейшей пошаговой отладки):
: gcd
begin
dup
while
tuck
mod
repeat
drop
;main.4th:
: forth_main
5 countdown
cr
10 6 gcd .
35 75 gcd .
12856 3248 gcd .
cr
;Можно выполнить эту программу с помощью скрипта оболочки:
./forth2c.py countdown.4th gcd.4th main.4th
gcc -g -static -Wall -o forth_program forth_program.c
./forth_program
Вывод должен получиться таким:5 4 3 2 1
2 5 8Итак, функция обратного отсчета (
countdown) исправно отсчитывает от 5 до 1, а gcd успешно находит наибольший общий делитель (НОД). Все молодцы.Отладка
Опробуем нашу программу с помощью доверенного
gdb:$ gdb forth_program
There is NO WARRANTY!! etc. etc.
(gdb) b countdown # помещаем точку останова
Breakpoint 1: file countdown.4th, line 3.
(gdb) r # выполняем
Breakpoint 1, countdown (s=0x804e03c) at countdown.4th:3
(gdb) bt # обратная трассировка
#0 countdown (s=0x804e03c) в countdown.4th:3
#1 forth_main (s=0x804e03c) в main.4th:2
#2 main () в forth_runtime.h:14
(gdb) l # вывод исходного кода
1 : COUNTDOWN
2 BEGIN
3 DUP .
4 1 -
5 DUP 0 =
6 UNTIL
7 DROP
8 ;
(gdb)Круто!
Действительно, круто. Мы установили точку останова, получили стек вызовов –
forth_main, названный countdown, и увидели исходный код Forth отлаживаемой функции – все это в отладчике, который понятия не имеет о Forth.Отчасти причина в компиляции в высокоуровневые примитивы, такие как функции, которые поддерживаются инструментами – это бы у нас получилось в любом языке. А отчасти связано с
#line – которая есть не везде. Эта возможность просто восхитительна – с помощью
#line мы сообщаем отладчикам, откуда на самом деле берется код ассемблера – не промежуточный код Си, а реальный исходный. Профилировщики и другие инструменты
Инструкцию
#line понимают не только отладчики – это относится и к профилировщикам, и к программам проверки, и т.д.А вот вывод KCachegrind, показывающий профиль нашей программы Forth, полученный через фреймворк Valgrind:
valgrind --tool=callgrind ./forth_program
kcachegrind `ls -t callgrind* | head -1`
Разве не здорово?
Компиляторы Си также в курсе
#line – чем мы можем воспользоваться, переложив выдачу сообщений об ошибках на компилятор. Например, если использовать неопределенное слово do_something, мы получим указание на ошибку с точностью до виновной в ней строки исходного кода:main.4th:3: implicit declaration of function ‘do_something’Очень чувствительное сообщение об ошибке – и делать для этого ничего не пришлось. А теперь попробуем
BEGIN без REPEAT, для чего удалим REPEAT из countdown.4th:gcd.4th:1: error: expected ‘while’ before ‘data’Хм, не очень хорошее сообщение. Возможно, все же идея была не очень удачной. Если нас интересует качественный ��тчет об ошибках, то их проверку нужно выполнять на уровне исходного языка.
Настраиваемое отображение данных: функции «красивой» печати в gdb
Все выглядит довольно неплохо. Инструменты Си – gdb, Valgrind, KCachegrind – с нашими задачами справляются, не так ли?
За исключением стека данных. Вместо элементов стека gdb показывает нам указатель TOS в hex-формате:
countdown (s=0x804e03c), forth_main (s=0x804e03c), что выглядит не очень удачно.Не то, чтобы локальная переменная
s бесполезна – наоборот, именно ее мы используем для просмотра стека данных. Это можно сделать с помощью команд gdb вроде p s[0] (выводит TOS), p s[1] (выводит элемент, следующий за TOS) и т.д. Но нам куда интереснее наблюдать весь стек сразу, чтобы видеть, как по мере нашего прохождения через код TUCK подбирает числа в стек.Можно ли это реализовать?
- Хорошая новость в том, что это достаточно легко сделать с помощью функций «красивой» печати. (Лично для меня это стало легко после того, как Noam сказал мне о таких функциях).
- А плохая в том, что, в отличие от повсеместно поддерживаемой
#line, нельзя выполнить кастомную «красивую» печать общим способом для всех инструментов. Не существует «#lineдля «красивой» печати» — очень жаль. - Но есть и еще одна хорошая новость. А именно то, что фронтенды gdb вроде KDevelop или Eclipse в определенной степени поддерживают «красивую» печать. (KDevelop с ней справляется лучше, чем Eclipse). Так что вам не нужно писать плагин GUI или нечто столь же ужасное. Дно стека известно постоянно – это указатель на область за глобальным массивом
stack[].
(Если бы у нас были потоки, этот массив был бы локальным для потока, но смысл вы поняли).
Итак, вот
gdb_forth.py:class StackPrettyPrinter(object):
bottom = None
bot_expr = 'stack + sizeof(stack)/sizeof(stack[0])'
def __init__(self,s):
self.s = s
def to_string(self):
if not self.bottom:
self.bottom = gdb.parse_and_eval(self.bot_expr)
s = self.s
i = 0
words = []
while s[i].address != self.bottom:
words.append(str(s[i]))
i += 1
return ' '.join(reversed(words))
def stack_lookup_func(val):
if str(val.type) == 'data *':
return StackPrettyPrinter(val)
gdb.pretty_printers.append(stack_lookup_func)
print 'loaded Forth extensions'gdb.pretty_printers – это список функций, которые решают, нужно ли обернуть передаваемое им gdb.Value в объект класса «красивой» печати. Как правило, это решение принимается на основе типа значения. gdb.parse_and_eval возвращает gdb.Value, представляющее значение заданного выражения Си. У gdb.Value есть тип, адрес и т.д. Если gdb.Value является указателем, можно индексировать его как список Python: val[ind], а если структурой, то использовать как словарь: val['member_name'] (примера для этого случая у нас здесь нет).Если вас заинтересовали возможности «красивой» печати gdb, то вот пара примечаний:
- Получение значений из других значений намного быстрее, чем
parse_and_eval, которая просто перегружает процессор. Так что освоить своеобразный синтаксис обращения кgdb.Valueочень даже стоит. - Вы можете возвращать вместо простых строк «карты» или «массивы». Так работает «красивая» печать для карт STL и векторов. Это можно использовать для вывода высокоуровневых структур данных, таких как динамические объекты (если вам повезет, и ваш дизайн совпадет с видением gdb – я тут нашел немало подвохов в деталях).
Что ж, давайте протестируем нашу функцию «красивой» печати – но сначала зарегистрируем ее в
~/.gdbinit:python execfile('/your/path/to/gdb_forth.py')А теперь:
$ gdb forth_program
There is NO WARRANTY!!
loaded Forth extensions # ага, вот наш вывод!
(gdb) b gcd
(gdb) r
Breakpoint 1, gcd (s=10 6) # а вот и стек
(gdb) python for i in range(4): gdb.execute('c')
# продолжаем 4 раза
Breakpoint 1, gcd (s=6 4)
Breakpoint 1, gcd (s=4 2)
Breakpoint 1, gcd (s=2 0)
# это были шаги gcd(10,6)
Breakpoint 1, gcd (s=35 75)
3 dup
# новые входные данные - gcd(35,75). Прошагаем:
(gdb) s # шаг (выполнение DUP)
4 while
(gdb) p s # вывод s
$1 = 35 75 75 # DUP повторила 75.
(gdb) s # шаг (выполнение WHILE)
5 tuck
(gdb) p s
$2 = 35 75 # ...а WHILE 75 извлекла.
(gdb) s # теперь выполняем могучую TUCK:
6 mod
(gdb) p s
$3 = 75 35 75 # Подбор произведен!
(gdb) bt # а как выглядит стек данных main?
#0 gcd (s=75 35 75) в gcd.4th:6
#1 forth_main (s=75 35) в main.4th:5
# выглядит он короче – вроде в разумных пределах
# (Указатель TOS в main остается неизменным, пока gcd не сделает возврат)Считаю, что вполне неплохо.
10 6, 6 4, 4 2, 2 0 – довольно лаконичная прогулка по алгоритму Эвклида в Forth.KDevelop с отображением стека
И, наконец, так выглядит KDevelop (где стек, отображаемый в GUI, изменяется с каждым шагом, чего мы и ожидаем от хорошего отладчика). Стек показан в окне Variables слева.

Для того, чтобы все заработало в KDevelop, ничего особенного делать не нужно, разве что сообщить отладчику о необходимости обработать пользовательский исходный код, как это иногда делается с программами Си.
Возможности, которые не отображаются в Си гармонично
Некоторые возможности исходного языка отображаются в Си более естественно, чем другие. Если попробовать задействовать не Forth, а другой язык, или использовать его весь, а не выборку, то некоторые возможности вызовут серьезные сложности.
Какие-то из них можно без проблем обработать с помощью другого промежуточного языка. Например, атрибуты динамических данных более естественно отображаются в JavaScript, чем в Си.
Но во многих случаях выбор промежуточного языка делается не на основе его совместимости с исходным. К примеру, если ваша цель браузеры, тогда и выбор должен наверняка пасть на JS, даже если он окажется плохим компаньоном для исходного языка. То же касается и других ситуаций, когда нужно выбирать Си или JVM, или .NET и т.д.
На деле многие разработчики языков намеренно сделали их гармонично сочетающимися именно с соответствующей платформой – промежуточным языком. Некоторые указывают на плавную интеграцию с этой платформой в самом названии языка: C++, Objective-C, CoffeScript, TypeScript. Они знают, что многие пользователи скорее выберут их язык из-за его платформы, чем по какой-либо другой причине, потому что они ей ограничены.
С учетом этого я приведу несколько возможностей, которые не переводятся лаконично в Си, и попутно опишу, как можно это решить:
- Динамическая привязка: здесь все довольно просто. Сгенерируйте нечто подобное
call(object,method_id)илиcall(object,"method_name"),multidispatch(method,obj1,obj2)– что предпочтете. Должно сработать. - Динамическая кодогенерация –
evalи т.д.: в Си нетeval, но естьfopen("tmp.c"),fwrite, system("gcc -o tmp.so tmp.c …"),dlopen("tmp.so")иdlsym. Это может дать вам неплохое подобиеeval. Для реализации потребуется непортируемый код, но совсем немного. В качестве альтернативы, если относительно медленнаяevalс относительно слабой поддержкой отладки вас устроит (часто так и бывает), то можете реализовать эту функцию с помощью интерпретатора, который сможет воспользоваться парсером вашего статического компилятора. Мы задействуем оба этих метода в нескольких местах, и все работает прекрасно. - Динамические данные: Генерация кода сложностей не представляет –
access(object,"attr_name")или любой другой вариант. Самое неприятное – это просматривать такие объекты в отладчиках. Один из подходов – это использовать «красивую» печать или написать скрипт аналогичного отладчика. Другой вариант – для «достаточно статических данных» — это сгенерировать определения структур Си в среде выполнения, скомпилировать их в общую библиотеку с информацией об отладке и загрузить эту библиотеку, как в случае с реализациейeval. - Исключения: я для этого использую
setjmp/longjmp. Думаю, именно так работалcfront. - Сбор мусора: как это ни смешно, но он может неплохо работать. Для Си существуют сборщики мусора, например Boehm collector. Работают ли они для программ Си? Не совсем – они могут принимать похожий на указатель байтовый паттерн за реальный указатель, что приводит к утечке. Но у вас этой проблемы нет, потому что ваша программа Си сгенерирована из кода, который компилятор может проанализировать и сообщить сборщику, где искать указатели. Так что, хоть сбор мусора и работает в Си неполноценно, он может отлично работать для языков, реализованных поверх Си.
- Дженерики, перегрузка и т.д.: большая часть из всего этого обрабатывается компилятором, и единственной проблемой становится декорирование имен. Вы можете декорировать их на свое усмотрение либо же следовать соответствующему соглашению С++, чтобы отладчики затем раздекорировали эти имена обратно в
collection<item>или нечто аналогичное. (Соглашение о декорировании имен в С++ не портируемо, поэтому вам может потребоваться поддержка нескольких их вариантов). - Несколько возвращаемых значений: мы передаем указатели на все возвращаемые значения, кроме первого. Я считаю, что возвращение структуры может упростить просмотр кода в отладчиках. Конечно, такое соглашение о вызовах окажется менее эффективным в сравнении с компилятором, генерирующим ассемблер, а не Си. Но при этом оно все же будет удачнее, чем реальная альтернатива, например кортеж Python.
- Сокращение программы: (например, преобразовать
(+ (* 2 3) 5)в(+ 6 5), а затем в11): ой-ой. Думаю, можно было бы написать на Си интерпретатор для выполнения этого, но от оптимизатора Си вы не получите столько пользы, и вообще никакой пользы не получите от отладчиков Си, профилировщиков и т.д. Базовая модель вычисления должна быть достаточно близка к Си, чтобы получить от цепочки инструментов этого языка не только портативность кода Си, реализующего ваш язык. (Я имею ввиду не то, что вам однозначно нужно создать представление в памяти для(+ 6 5)в среде выполнения – а то, что если вы захотите его создать, то инструменты Си не поймут вашу программу).
А как насчет использовать С++ вместо Си?
Вы можете это сделать, и я даже попробовал. В итоге – не советую. Вместо возни с
setjump/longjump вы на халяву получаете исключения. В результате, когда вы выбрасываете их из одной общей библиотеки и перехватываете в другой, то они не работают (я думаю, что RTTI* имеет аналогичные проблемы с общими библиотеками). Вы также получаете халявные шаблоны, и в итоге ваш автогенерируемый код компилируется вечность. *RTTI – динамическая идентификация типа данных.
Эли Бендерски писал о переходе с Си на С++ для реализации языковой виртуальной машины. Он говорит, что когда возможности С++ соответствуют нуждам вашей виртуальной машины, то проще использовать их, чем переписывать альтернативу на Си.
В моем же случае это привело вот к чему. Однажды я реализовал VM и интерпретатор на С++. Затем мы написали для этого языка компилятор. Поскольку VM была написана на С++, компилятору было намного проще для взаимодействия с ней генерировать код С++, чем код Си. В итоге из-за этого автогенерируемого кода С++ мы столкнулись с проблемами работы общих библиотек, проблемами со скорост��ю сборки и другими, которые остались по сей день.
Конечно, это не правило, и ваш опыт может отличаться. Одна из причин использовать С++ в том, что отладчики все лучше отображают коллекции STL и указатели на объекты базового класса (которые они не забывают привести к реальному типу среды выполнения и показать членов производного класса). Если конструкции вашего языка сочетаются с этими возможностями С++, то вы получите улучшенное отображение данных отладки портативным способом.
Заключение
Си отлично подходит в качестве промежуточного языка, и я буду рад увидеть варианты компиляции в него других языков. В последнее время я не работаю ни с Java, ни с JavaScript. Определенно, не я один. Язык, компилируемый в Си, может не только быть быстрым, портативным, обладать хорошими инструментами и поддержкой библиотек, но также оказаться пригодным для многих, работающих с Си, С++ и Objective-C.

