Как защитить собственный стек вызовов интерпретатора от сборки мусора? - PullRequest
0 голосов
/ 14 октября 2018

Я пишу интерпретатор Lisp на C. Каждый объект Lisp представлен struct LispObject * с полем type, чтобы указать, является ли оно int, символом, минусом и т. Д. Я реализовал глобальную среду какхеш-таблица, содержащая пары имен и значений.

LispObject s всегда динамически выделяются с malloc.Каждый раз, когда создается новый объект, он добавляется в список слабых ссылок.При запуске сборщика мусора он помечает все объекты, доступные из глобальной среды, а затем удаляет слабые ссылки и освобождает немаркированные объекты.

Глобальную среду легко защитить от сбора мусора.Я застрял на том, как защитить локальные объекты Lisp.Чтобы было ясно, я еще не реализовал функции Lisp.Я спрашиваю, как защитить локальные переменные C типа LispObject *.Например, eval - это функция C, которая принимает выражение LispObject *, применяет правила оценки и возвращает значение LispObject *.Мне нужно защищать локальные LispObject * переменные в eval (и другие функции C, которые имеют дело с объектами Lisp) от сборки мусора до тех пор, пока функция не вернется.

Каков был бы самый чистый способ сделать это?Есть ли какой-нибудь способ отметить какие-либо LispObject s, которые достижимы из стека вызовов C?

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

Полный код: https://notabug.org/jtherrmann/lisp-in-c

1 Ответ

0 голосов
/ 14 октября 2018

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

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

Возможностьможет быть явно ваши локальные кадры как некоторые struct.Посмотрите, например, на то, что делает среда выполнения Ocaml (прочитайте ее раздел §20.5 Жизнь в гармонии с сборщиком мусора ) или на мой старый (необслуживаемый) Qish GC.Например, вы можете принять соглашение о том, что каждый локальный кадр интерпретатора находится в некоторой локальной переменной _ (a struct), и использовать это.В моем проекте bismon я бы кодировал что-то, почти эквивалентное (после расширения препроцессора) этому, для подпрограммы C crout, имеющей аргумент-указатель a и два локальных указателя b и c

 void crout(struct callingframe_st *cf, LispObject*a) {
   struct mycallframe_st {
      struct callingframe_st* from;
      int nbloc;
      LispObject* aa;
      LispObject* bb;
      LispObject* cc;
   } _;
   memset(&_, 0, sizeof(_));
   _.from = cf;
   _.nbloc = 3; // the current frame has 3 locals: aa, bb, cc
   _.aa = a;
   #define a _.aa
   #define b _.bb
   #define c _.cc

Затем следует тело crout.Это передало бы (struct callingframe_st*)(&_) соответствующим подпрограммам.В конце убедитесь, что #undef a и т. Д. ... Ваш GC, вызванный из ваших процедур распределения, должен принять (struct callingframe_st *)(&_) в качестве аргумента (давая текущий кадр вызова).

Так что, конечно, ваш b_cons при условии, что он может косвенно вызвать ваш GC, должен быть объявлен как

LispObject* b_cons(struct callingframe_st*cf, 
                    LispObject * car, LispObject * cdr);

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


Вам необходимо понять, как работает сборщик мусора (и разницу между точным и консервативным сборщиком мусора).Я настоятельно рекомендую прочитать справочник GC или хотя бы старую бумагу Пола Уилсона Uniprocessor Garbage Collection .Вы можете принять соглашение о том, что все ваши подпрограммы следуют стилю A-нормальной формы (поэтому вы никогда не кодируете напрямую в C f(g(x),h(x,y)) со всеми f, g, h, возможно, выполняющими объектраспределение).

Вы также можете использовать некоторые существующие точные GC, такие как Ravenbrook MPS .

В противном случае, используйте некоторые консервативные GC как GC Боэма .

Смотрите также исходный код существующих интерпретаторов свободного программного обеспечения, имеющих немного GC.

Читайте также Queinnec's LispВ Small Pieces book


Мне нужно вручную толкать и вставлять объекты, чтобы вызывать функции C.

Это может бытьхорошая идея (но тогда вам нужно переписать большую часть кода, и вы действительно можете определить свой собственный байт-код механизм).Посмотрите, что Lua или Nim или Ocaml интерпретатор байт-кода или Emacs Elisp интерпретатор делает.


В завершение вы можете подумать (это действительно сложно, и я не рекомендую идти по этому пути, поскольку это займет много лет работы) написание некоторого GCC плагина для генерации и / или добавления ad-hocметаданные фрейма вызова и / или сгенерируйте код, связанный с фреймом вызова, чтобы помочь вашему точному GC.Это действительно сложно.IIRC, CLASP делает нечто подобное (выше Clang, а не GCC).


Не забывайте, что сборка мусора - это целая программа .

...