Autor Wątek: Nieblokująca Lua  (Przeczytany 7516 razy)

Offline Xirdus

  • Redaktor

# Czerwiec 21, 2014, 17:04:48
Chodzi o to, że skrypty mają być maksymalnie proste do pisania, czyli m.in. minimum (jak się da to zero) boilerplate'u i możliwość korzystania przynajmniej ze wszystkich podstawowych konstrukcji programistycznych, a najlepiej ze wszystkiego co oferuje Lua. Przy czym skrypty będą zasadniczo tylko łącznikami między C++-owymi funkcjami, jednak objętościowo może być ich tyle samo, jak nie więcej niż kodu silnika.

Przykładowy przykład w pseudokodzie:
function rozmowa_z_kolesiem_w_karczmie()
{
    wyswietl_dialog dialog1
    wyswietl_dialog dialog2
    wybor = wyswietl_dialog_z_opcja_wyboru dialog3 opcja1 opcja2
    if (wybor == 1) {
        wyswietl_dialog opcja1_dialog1
        wyswietl_dialog opcja1_dialog2
        if (ilosc_itemow("moneta") >= 10) {
            wyswietl_dialog quest_wykonany_dialog1
            rusz_postacia_async postac1 gora 10
            rusz_postacia postac2 gora 10
            wyswietl_dialog quest_wykonany_dialog2
            quest_wykonany = true
        } else {
            wyswietl_dialog quest_niewykonany_dialog1
        }
    } else if (wybor == 2) {
        if (rozpocznij_walke grupa_trzech_nietoperzy) {
            wyswietl_dialog walka_wygrana_dialog1
        } else {
            wyswietl_dialog walka_przegrana_dialog1
            ulecz_druzyne
    }
}

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Xion

  • Moderator
    • xion.log

# Czerwiec 21, 2014, 17:29:15
Czyli tak naprawdę kodujesz schemat przejść stanów, ale z jakiegoś powodu nie chcesz mieć go zapisanego deklaratywnie (z ewentualnymi wstawkami z kodem), tylko w całości jako imperatywny kod. Okej, da się zrobić -- potrzebujesz do tego dokładnie tego samego mechanizmu, którego używają systemy operacyjne do preemptive multitaskingu, czyli wywłaszczania procesu (w tym przypadku skryptu) gdy wywoływane jej systemowe API.

Tym mechanizmem są coroutines. Żeby lepiej zrozumieć analogię "yield == API call", polecam przejrzeć tę prezentację, zwłaszcza mniej więcej od połowy (tak, wiem że Python, ale idea jest identyczna).

EDIT: Powtarzam aczkolwiek, że to nie jest jedyna opcja. Twój skrypt da się też zapisać z callbackami. Oto przykład w czymś w rodzaju JS, gdzie series() to async.series() z async.js:
function talkWithTheGuyInTheTavern() {
  series(showDialog1, showDialog2);
  showChoiceDialog(function(choice) {
    if (choice == 1) {
      series(showChoice1Dialog1, showChoice2Dialog2);
      if (itemsCount("coin") >= 10) {
          series(
            showQuestCompletedDialog1,
            function() { moveCharacter(char1, Up, 10); },
            function() { moveCharacter(char2, Up, 10); },
            showQuestCompletedDialog2,
            function() { questCompleted = true; });
      } else {
        showQuestIncompleteDialog1();
    } else if (choice == 2) {
      fight(creatures(Bat, 3), function(won) {
        if (won) {
          showFightWonDialog1();
        } else {
          showFightLostDialog1();
          healParty();
        }
      });
    }
  });
}
Łatwiej pewnie skodzić takie API niż te oparte na coroutine'ach, ale kosztem jest oczywiście asynchroniczny flow.
« Ostatnia zmiana: Czerwiec 21, 2014, 17:45:43 wysłana przez Xion »

Offline Xirdus

  • Redaktor

# Czerwiec 21, 2014, 18:13:39
Czyli tak naprawdę kodujesz schemat przejść stanów, ale z jakiegoś powodu nie chcesz mieć go zapisanego deklaratywnie (z ewentualnymi wstawkami z kodem), tylko w całości jako imperatywny kod. Okej, da się zrobić -- potrzebujesz do tego dokładnie tego samego mechanizmu, którego używają systemy operacyjne do preemptive multitaskingu, czyli wywłaszczania procesu (w tym przypadku skryptu) gdy wywoływane jej systemowe API.

Tym mechanizmem są coroutines. Żeby lepiej zrozumieć analogię "yield == API call", polecam przejrzeć tę prezentację, zwłaszcza mniej więcej od połowy (tak, wiem że Python, ale idea jest identyczna).
No coś takiego właśnie. Z tym że wątek będzie jeden i tylko jeden - to nie może być aż tak trudne? (No chyba że jest.)

EDIT: Powtarzam aczkolwiek, że to nie jest jedyna opcja. Twój skrypt da się też zapisać z callbackami. Oto przykład w czymś w rodzaju JS, gdzie series() to async.series() z async.js:
Nie jestem do końca przekonany czy wrapowanie każdej jednej linijki kodu w funkcję będzie wygodne. Albo mogę zrobić jak laggyluk powiedział, dodać preprocesor który te wszystkie funkcje wygeneruje. Pytanie czy warto?

Offline Xion

  • Moderator
    • xion.log

# Czerwiec 21, 2014, 18:41:30
Cytuj
Pytanie czy warto?
Cóż, jeśli chcesz swój synchroniczny cukierek składniowy, masz dwie możliwości:

* Możesz się bawić w lua_yield/lua_resume, żeby obsługiwać coroutines. Twój przypadek jest z tego co widzę w miarę prosty, bo (1) masz jeden wątek (2) yield będziesz wywoływał tylko z C, a nie z Lua, więc AFAICS potrzebujesz tylko lua_resume i lua_yield.

* Możesz się bawić w ów preprocesor, co jest złożonościowo równoważne... napisaniu własnego języka skryptowego :)

Offline lethern

  • Użytkownik

# Czerwiec 21, 2014, 19:16:10
Cytuj
* Możesz się bawić w ów preprocesor, co jest złożonościowo równoważne... napisaniu własnego języka skryptowego :)
Why? Napisanie własnego języka skryptowego brzmi trochę poważniej niż prosty parser i maszyna stanów

Dlaczego tak mi się wydaje: jak kiedyś napisałem "preparser" do c++, który analizował komentarze przed funkcjami i w odpowiednich miejscach w funkcji wrzucał logowania (na początku, przed końcem, przed return; i wrapował w { }) to nie było to zupełnie porównywalne z parserem c++ ani pisaniu własnego c++, prosty parser który miał obejmować subset języka

Offline Xion

  • Moderator
    • xion.log

  • +1
# Czerwiec 21, 2014, 19:58:36
Cytuj
(...) nie było to zupełnie porównywalne z parserem c++ ani pisaniu własnego c++, prosty parser który miał obejmować subset języka
Bo i to co chciałeś zrobić nie było porównywalne.

Cytuj
(...) trochę poważniej niż prosty parser i maszyna stanów.
Bo to nie jest prosty parser.

O ile:
asyncOp1();
asyncOp2();
dość łatwo przekłada się na:
asyncOp1(asyncOp2);o tyle:
asyncOp();
/* jakieś instrukcje */
wymaga już dodania nowej anonimowej funkcji, zaś:
result = asyncOp();
/* jakieś operacje na result */
to już funkcja z parametrami, i wreszcie:
asyncOp1(asyncOp2() + asyncOp3(arg1, arg2));to już nie tylko przepisanie całego wyrażenia, ale też szansa kolizji nazw wewnątrz anonimowej funkcji, którą dodajemy, i przed którą trzeba się zabezpieczyć.

Itp., itd. Ogólnie sama konwersja to problem podobny do transformacji kodu do formy pośredniej jaką robią kompilatory (śledzenie przepływu danych między zmiennymi), a do tego dochodzi jeszcze sparsowanie języka wejściowego.

Zdecydowanie polecam już bawić się w coroutines :)

Offline Paweł

  • Użytkownik

# Czerwiec 21, 2014, 23:09:44
auto scriptResult = std::async(std::launch::async, [L](){
  lua_getglobal(L, "foo");
  lua_pcall(L, 0, 1, 0);
  int ret = lua_tonumber(L, -1);
  lua_pop(L, 1);
  return ret;
});
render();
int scriptState = scriptResult.get();
Przy okazji dodam że wywołania funkcji lua z c++11 można sobie bardzo uprościć używając variadic templates.
Proponowany interfejs:
constexpr LuaFunction<int(std::string, int)> foo("foo");
int result = foo(L, "asd", 42);
Czy byłby ktoś zainteresowany taką libką? Bo pomysł mi się spodobał i pewnie go zaimplementuje :)

Offline Xirdus

  • Redaktor

# Czerwiec 21, 2014, 23:27:41
...
Czyli dwójka z OP. Tak się właśnie zastanawiałem że przez wątek przewinęło się parę rozwiązań, ale nikt nie wspomniał nic o wątkach - odebrałem to jako znak :) Jak nie uda mi się zabawa z yield to tak pewnie zrobię.

Czy byłby ktoś zainteresowany taką libką?
Osobiście nie - w tej libce co wcześniej podlinkowałem, z której korzystam, wygląda to tak:
LuaIntf::LuaRef func = context.globals()["func"];
func.call<void>("func");
(void to typ zwracany wywoływanej funkcji - jak się nie zgadza, albo jak wystąpi jakiś inny błąd, leci wyjątek.) Jak dla mnie good enough - jak by co mogę zawsze zrobic std::bind. Ale konkurencji na rynku libek nigdy za wiele ;) Ale ja bym w twoim przykładzie zmienił LuaFunction na std::function.

Offline Paweł

  • Użytkownik

# Czerwiec 21, 2014, 23:34:34
LuaFunction w moim przykładzie to ceplusplusowa deklaracja funkcji zdefiniowanej w lua.

Znalazłem właśnie podobną likbę, choć interfejs nieco inny:
http://www.jeremyong.com/blog/2014/01/10/interfacing-lua-with-templates-in-c-plus-plus-11/

Offline Xirdus

  • Redaktor

# Czerwiec 22, 2014, 00:45:06
Właśnie natrafiłem na problem - żeby móc użyć lua_yield(), trzeba mieć coroutine, a wrapper z którego korzystam coroutines nie wspiera (tzn. pozwala na bezpośrednią zabawę oryginalnymi funkcjami Lua - to równie dobrze mogę nie mieć żadnego wrappera). Pobawię się trochę gołym API, może coś z tego wyniknie - a jak nie to przejdę na synchroniczne wątki.

Offline Xirdus

  • Redaktor

# Czerwiec 22, 2014, 02:11:28
Właśnie natrafiłem na jeszcze większy problem:
you cannot yield across C call boundary. That is, when you call lua_pcallk and the chunk that pcallk is calling yields, the continuation function is executed. However, you cannot have lua_pcallk call a Lua function that in turn calls a C function that yields (pause in your example). That's forbidden.
I cały misterny plan wpizdu... Tak więc albo zrobię jak mówi Xion i będą callbacki, albo zrobię to jakoś na wątkach i semaforach, albo nauczę się LuaJIT bo on na takie coś pozwala.

Offline Xion

  • Moderator
    • xion.log

# Czerwiec 22, 2014, 11:59:48
Dlaczego chcesz koniecznie używać lua_pcallk? Większość przykładów, jakie udało mi się znaleźć, używały lua_resume, która nie ma problemu z yieldami.

Offline Dab

  • Redaktor
    • blog

# Czerwiec 22, 2014, 13:52:37
wrapper z którego korzystam coroutines nie wspiera (

A po co w ogóle wrapper? Większość wrapperów robi z Lua jakieś dziwadło łączące wady Lua z wadami C++. Lua z powodzeniem można używać korzystając z bazowego API, dodając sobie jedynie potrzebne funkcje lua_pushXXX/lua_toXXX i ew. korzystając z pomocniczej funkcji do tworzenia/czytania tablic.

Offline Xirdus

  • Redaktor

# Czerwiec 22, 2014, 16:30:26
A jednak się udało z gołą Luą. W sumie to nie jest takie trudne - po prostu nie ma ani jednego porządnego tutoriala na ten temat.
#include <iostream>
#include <lauxlib.h>
#include <thread>

const char script[] = R"script(
function func()
    doPrint("log")
    doPrint("log", "log")
    doPrint("log", "log", "log")
end
)script";

int doPrint(lua_State* state)
{
    int num_args = lua_gettop(state);
    for (int i = 1; i <= num_args; ++i)
    {
        std::cout << luaL_checkstring(state, i) << ' ';
    }
    std::cout << std::endl;
    return lua_yield(state, 0);
}

int main()
{
    lua_State* main_state = luaL_newstate();
    luaL_dostring(main_state, script);
    lua_pushcfunction(main_state, &doPrint);
    lua_setglobal(main_state, "doPrint");
    lua_State* coroutine_state = lua_newthread(main_state);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    lua_getglobal(coroutine_state, "func");
    while (lua_resume(coroutine_state, nullptr, 0) == LUA_YIELD)
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}
Zgodnie z oczekiwaniami, powyższy kod co sekundę printuje jedną linijkę.


Mam jeszcze takie jedno pytanie - gdzieś w necie znalazłem, że "szeroko stosowaną" praktyką jest niekorzystanie z bibliotekowej wersji Lua, tylko dołączanie całego kodu źródłowego do swojego projektu, że niby pozwala to uniknąć problemów z wyjątkami czy czymś. Ma to w ogóle jakikolwiek sens? I czy warto kompilować Luę w trybie C++?

Offline Xion

  • Moderator
    • xion.log

# Czerwiec 22, 2014, 16:59:41
Cytuj
A jednak się udało z gołą Luą. W sumie to nie jest takie trudne - po prostu nie ma ani jednego porządnego tutoriala na ten temat.
Zaprawdę powiadam ci: jeśli do każdej tego typu drobnostki będziesz oczekiwał tutoriala, to będziesz posuwał się baaardzo powoli. Nie wiem też, co znaczy to "a jednak" -- wskazałem ci na dwie funkcje, których powinieneś użyć, użyłeś ich, działa -- nic w tym dziwnego :)

Pamiętaj tylko, że rozwiązałeś póki co prostszy problem. Będziesz musiał jeszcze przestrukturyzować pętle główną w swojej grze i napisać API, które będzie się w nią odpowiednio wgryzało. Czuję tu nosem jakąś kolejkę eventów lub coś tym guście.