In einem Blogbeitrag von vor nicht allzu langer Zeit beschreibt Scott Vokes ein technisches Problem verbunden mit luas Implementierung von Koroutinen unter Verwendung der C-Funktionen setjmp
und longjmp
:
Die Haupteinschränkung von Lua-Routinen besteht darin, dass sie, da sie mit setjmp (3) und longjmp (3) implementiert sind, nicht dazu verwendet werden können, um von Lua in C-Code zu rufen, der zurück in Lua ruft, das in C zurückruft, weil Der verschachtelte longjmp wird die Stack-Frames der C-Funktion überlisten. (Dies wird zur Laufzeit erkannt und nicht im Hintergrund ausgeführt.)
Ich habe nicht festgestellt, dass dies ein Problem in der Praxis ist, und ich weiß nicht, wie ich es reparieren kann, ohne Lua's Portabilität zu beschädigen, eines meiner Lieblingsdinge an Lua - es wird buchstäblich alles mit ANSI laufen C-Compiler und eine bescheidene Menge an Speicherplatz. Mit Lua kann ich leicht reisen. :)
Ich habe Coroutines in einer angemessenen Menge verwendet und ich dachte, ich verstehe im Großen und Ganzen, was los war und was setjmp
und longjmp
tun, aber ich las dies irgendwann und erkannte, dass ich es nicht wirklich verstand. Um zu versuchen, es herauszufinden, habe ich versucht, ein Programm zu machen, von dem ich dachte, dass es aufgrund der Beschreibung ein Problem verursachen sollte, und stattdessen scheint es gut zu funktionieren.
Allerdings gibt es ein paar andere Orte, an denen ich Leute gesehen zu haben scheinen, dass es Probleme gibt:
Die Frage ist:
Hier war der Code, den ich produziert habe. In meinem Test ist es mit lua 5.3.1 verknüpft, kompiliert als C-Code, und der Test selbst wird als C ++ - Code nach C ++ 11-Standard kompiliert.
%Vor%Die Ausgabe, die ich bekomme, ist:
%Vor% Vielen Dank an @ Nicol Bolas für die sehr detaillierte Antwort!
Nachdem ich seine Antwort gelesen, die offiziellen Dokumente gelesen, einige E-Mails gelesen und etwas mehr damit herumgespielt habe, möchte ich die Frage präzisieren / eine bestimmte Folgefrage stellen, wie auch immer Sie sie ansehen möchten.
Ich denke, dieser Begriff "Clobbering" ist nicht gut für die Beschreibung dieses Problems, und das war ein Teil dessen, was mich verwirrte - nichts wird "durchnässt" in dem Sinne, dass es zweimal geschrieben wird und der erste Wert verloren geht ist nur, wie @Nicol Bolas darauf hinweist, dass longjmp
einen Teil des C-Stacks wirft, und wenn Sie hoffen, den Stack später wiederherzustellen, schade.
Das Problem wird in Abschnitt 4.7 des Lua 5.2-Handbuchs sehr gut beschrieben Link von @ Nicol Bolas zur Verfügung gestellt.
Seltsamerweise gibt es in der lua 5.1-Dokumentation keinen entsprechenden Abschnitt. Jedoch hat lua 5.2 dies zu sagen über lua_yieldk
:
Liefert eine Coroutine.
Diese Funktion sollte nur als Rückgabeausdruck einer C-Funktion wie folgt aufgerufen werden:
return lua_yieldk (L, n, i, k);
Lua 5.1-Handbuch sagt etwas Ähnliches , etwa lua_yield
stattdessen:
Liefert eine Coroutine.
Diese Funktion sollte nur als Rückgabeausdruck einer C-Funktion wie folgt aufgerufen werden:
return lua_yieldk (L, n, i, k);
Einige natürliche Fragen dann:
return
hier verwende oder nicht? Wenn lua_yieldk
longjmp
aufruft, dann wird lua_yieldk
niemals zurückgeben, also sollte es egal sein, ob ich dann zurückkomme? Das kann also nicht geschehen, oder? lua_yieldk
nur eine Notiz im lua-Zustand macht, dass der aktuelle C-API-Aufruf angegeben hat, dass er nachgeben will, und wenn es dann endlich zurückkehrt, wird lua herausfinden, was als nächstes passiert. Dann löst dies das Problem des Speicherns von C-Stapelrahmen, nein? Da, nachdem wir normalerweise nach Lua zurückgekehrt sind, diese Stack Frames sowieso schon abgelaufen sind - also werden die im @ Nicol Bolas Bild beschriebenen Komplikationen umgangen? Und zweitens, in 5.2 zumindest ist die Semantik nie, dass wir C Stack Frames wiederherstellen sollten, es scheint - lua_yieldk
wird zu einer Fortsetzungsfunktion fortgesetzt, nicht zum lua_yieldk
Aufrufer, und lua_yield
wird augenscheinlich wieder zur Aufrufer des aktuellen api-Aufrufs, nicht an den lua_yield
-Aufrufer selbst. Und die wichtigste Frage:
Wenn ich
lua_yieldk
in der in den Dokumenten angegebenen Formreturn lua_yieldk(...)
konsistent verwende und von einemlua_CFunction
, das an lua übergeben wurde, zurückkehrt, ist es weiterhin möglich, den Fehlerattempt to yield across a C-call boundary
auszulösen?
Schließlich, (aber das ist weniger wichtig), würde ich gerne ein konkretes Beispiel sehen, wie es aussieht, wenn ein naive Programmierer "nicht vorsichtig ist" und den attempt to yield across a C-call boundary
Fehler auslöst. Ich habe die Idee, dass es Probleme mit setjmp
und longjmp
geben könnte, die Stack-Frames werfen, die wir später brauchen, aber ich möchte einen echten lua / lua capi-Code sehen, auf den ich zeigen und sagen kann " tu das nicht ", und das ist überraschend schwer fassbar.
Ich habe diese E-Mail gefunden, wo jemand diesen Fehler mit einigen Lua 5.1 gemeldet hat Code, und ich habe versucht, es in lua 5.3 zu reproduzieren. Was ich jedoch fand, war, dass dies nur ein Fehlerbericht aus der lua-Implementierung ist - der eigentliche Fehler wird verursacht, weil der Benutzer seine Coroutine nicht richtig eingerichtet hat. Die richtige Methode zum Laden der Coroutine besteht darin, den Thread zu erstellen, eine Funktion auf den Thread-Stack zu setzen und anschließend lua_resume
im Thread-Status aufzurufen. Stattdessen verwendete der Benutzer dofile
auf dem Thread-Stack, der die Funktion dort nach dem Laden ausführt, anstatt sie wieder aufzunehmen. Also ist es effektiv yield outside of a coroutine
iiuc, und wenn ich dies patch, funktioniert sein Code gut, mit lua_yield
und lua_yieldk
in lua 5.3.
Hier ist die Liste, die ich erstellt habe:
%Vor% Hier ist die Ausgabe, wenn USE_YIELDK
auskommentiert ist:
Hier ist die Ausgabe, wenn USE_YIELDK
definiert ist:
Denken Sie darüber nach, was passiert, wenn eine Coroutine eine yield
ausführt. Es stoppt die Ausführung, und die Verarbeitung kehrt zu dem zurück, der resume
in dieser Coroutine genannt wurde, richtig?
Nun, sagen wir, Sie haben diesen Code:
%Vor% Zum Zeitpunkt des Aufrufs von yield
sieht der Lua-Stack wie folgt aus:
Wenn Sie yield
aufrufen, wird der Lua-Aufrufstack, der Teil der Coroutine ist, beibehalten. Wenn Sie resume
ausführen, wird der beibehaltene Aufruf-Stack erneut ausgeführt und beginnt dort, wo er vorher unterbrochen wurde.
OK, jetzt sagen wir, dass middle
tatsächlich keine Lua-Funktion war. Stattdessen war es eine C-Funktion, und diese C-Funktion ruft die Lua-Funktion top
auf. So gesehen sieht dein Stack folgendermaßen aus:
Bitte beachten Sie, was ich vorher gesagt habe: so sieht Ihr Stack konzeptionell aus .
Weil Ihr tatsächlicher Aufrufstapel nicht so aussieht.
In Wirklichkeit gibt es wirklich zwei Stapel. Es gibt Lua's internen Stack, definiert durch lua_State
. Und da ist Cs Stack. Lua's interner Stack, zu dem Zeitpunkt, wenn yield
aufgerufen wird, sieht ungefähr so aus:
Also, wie sieht der Stack zu C aus? Nun, es sieht so aus:
%Vor% Und genau da ist das Problem. Sehen Sie, wenn Lua eine yield
macht, ruft sie longjmp
auf. Diese Funktion basiert auf dem Verhalten des C-Stacks. Es wird nämlich dorthin zurückkehren, wo setjmp
war.
Der Lua-Stapel wird beibehalten, da der Lua-Stapel vom C-Stapel getrennt ist. Aber der C-Stapel? Alles zwischen longjmp
und setjmp
?. Weg. Kaputt. Verlor für immer .
Nun kannst du gehen, "warte, weiß der Lua-Stapel nicht, dass er in C und zurück in Lua gegangen ist"? Ein bisschen. Aber der Lua-Stapel ist unfähig, etwas zu tun, zu dem C nicht in der Lage ist. Und C ist einfach nicht in der Lage, einen Stapel zu konservieren (gut, nicht ohne spezielle Bibliotheken). Während also der Lua-Stack vage weiß, dass ein C-Prozess in der Mitte seines Stacks stattgefunden hat, kann er nicht rekonstruieren, was da war.
Was passiert also, wenn Sie diese yield
ed-Coroutine fortsetzen?
Nasale Dämonen. Und niemand mag diese. Zum Glück, Lua 5.1 und höher (zumindest) wird Fehler, wenn Sie versuchen, über C zu liefern.
Beachten Sie, dass Lua 5.2+ Möglichkeiten hat, dies zu beheben . Aber es ist nicht automatisch; es erfordert eine explizite Codierung Ihrerseits.
Wenn Lua-Code, der sich in einer Coroutine befindet, Ihren C-Code aufruft und Ihr C-Code Lua-Code aufruft, können Sie lua_callk
oder lua_pcallk
verwenden, um die möglicherweise ergebenden Lua-Funktionen aufzurufen. Diese aufrufenden Funktionen nehmen einen zusätzlichen Parameter: eine "Fortsetzung" -Funktion.
Wenn der von Ihnen aufgerufene Lua-Code liefert, wird die lua_*callk
-Funktion niemals wirklich zurückkehren (da Ihr C-Stack zerstört wurde). Stattdessen wird die Fortsetzungsfunktion aufgerufen, die Sie in Ihrer lua_*callk
-Funktion angegeben haben. Wie Sie anhand des Namens erraten können, besteht der Job der Fortsetzungsfunktion darin, dort fortzufahren, wo Ihre vorherige Funktion nicht mehr aktiv war.
Nun behält Lua den Stapel für Ihre Fortsetzungsfunktion, so dass er den Stapel in demselben Zustand erhält wie Ihre ursprüngliche C-Funktion. Nun, außer dass die Funktion + Argumente, die Sie aufgerufen haben (mit lua_*callk
), sind entfernt, und die Rückgabewerte von dieser Funktion werden auf Ihren Stapel geschoben. Abgesehen davon ist der Stapel alle gleich.
Es gibt auch lua_yieldk
. Dadurch kann Ihre C-Funktion zu Lua zurückkehren, so dass bei Wiederaufnahme der Coroutine die angegebene Fortsetzungsfunktion aufgerufen wird.
Beachten Sie, dass Coco Lua 5.1 die Möglichkeit gibt, dieses Problem zu lösen. Es ist in der Lage (obwohl OS / Assembly / etc Magie) von Erhaltung der C-Stapel während einer Yield-Operation. LuaJIT-Versionen vor 2.0 bieten diese Funktion ebenfalls.
C ++ Hinweis
Sie haben Ihre Frage mit dem C ++ - Tag markiert, also nehme ich an, dass das hier eine Rolle spielt.
Zu den vielen Unterschieden zwischen C und C ++ gehört die Tatsache, dass C ++ weit mehr von der Art seines Callstacks abhängt als Lua. Wenn Sie in C einen Stapel verwerfen, können Ressourcen verloren gehen, die nicht bereinigt wurden. C ++ ist jedoch erforderlich, um Destruktoren von Funktionen aufzurufen, die auf dem Stapel an einem bestimmten Punkt deklariert sind. Der Standard erlaubt es nicht, sie einfach wegzuwerfen.
Fortsetzungen funktionieren also nur in C ++, wenn nothing auf dem Stack ist, der einen Destruktoraufruf benötigt. Oder genauer gesagt können nur trivial zerstörbare Typen auf dem Stapel sitzen, wenn Sie eine der Lua-APIs der Fortsetzungsfunktion aufrufen.
Natürlich behandelt Coco C ++ gut, da es tatsächlich den C ++ - Stack enthält.
Veröffentlichen Sie dies als eine Antwort, die @ Nicol Bolas Antwort ergänzt, und so Ich kann Platz haben, um aufzuschreiben, was ich gebraucht habe, um das Original zu verstehen Frage und die Antworten auf die sekundären Fragen / eine Code-Liste.
Wenn Sie die Antwort von Nicol Bolas gelesen haben, aber noch Fragen wie ich haben, hier sind einige zusätzliche Hinweise:
lua_callk
, lua_pcallk
, wodurch Sie einen Ersatz bereitstellen können
Funktion, die anstelle der C-Funktion aufgerufen werden kann, deren Frames waren
ausgelöscht. return lua_yieldk(...)
scheint nichts zu tun zu haben
irgendeins von diesen. Wenn Sie die Implementierung von lua_yieldk
überfliegen, scheint dies der Fall zu sein
es ist tatsächlich immer longjmp
, und es kann nur in irgendeinem obskuren Fall zurückkehren
mit Lua Debugging-Haken (?). nny
(Anzahl non-yieldable) zugeordnet wird
in den Lua-Zustand und wenn Sie lua_call
oder lua_pcall
von einem CAPI aufrufen
Funktion (a lua_CFunction
, die du vorher nach lua geschoben hast), nny
ist
inkrementiert und wird nur dekrementiert, wenn dieser Aufruf oder pcall zurückkehrt. Wann
nny
ist ungleich Null, es ist nicht sicher zu liefern, und Sie erhalten diesen yield across
C-api boundary
Fehler, wenn Sie versuchen, trotzdem zu liefern. Hier ist eine einfache Auflistung, die das Problem erzeugt und die Fehler meldet,
wenn du wie ich bist und gerne konkrete Code-Beispiele hast. Es demonstriert
einige der Unterschiede bei der Verwendung von lua_call
, lua_pcall
und lua_pcallk
innerhalb einer Funktion, die von einer Coroutine aufgerufen wird.
Beispielausgabe:
call
pcall
pcallk