sabato 4 agosto 2018

Python vs Lua (3)

Nelle puntate precedenti abbiamo parlato di micropython.
... e Lua?
Prima di tutto Lua c'entra perché è una delle opzioni sul tappeto avendo fama di essere un sistema di scripting poco esoso in termini di dimensioni dell'interprete ed inoltre facilmente integrabile in C.
Il limite è che lua è un linguaggio di scripting (con una sintassi anche opinabile) e non certo un linguaggio esteso e general-purpose come python. In merito, lasciate pure i vostri commenti e critiche qui sotto, grazie!
Ok, allora facciamo qualche prova...

Per fare qualche prova senza rischiare di sporcare il mio PC ho preferito usare vagrant
Se non lo avete ancora usato ve lo consiglio: la comodità di un intera macchina virtuale autocontenuta a riga di comando e costruita con due semplici istruzioni!
(Vagrant ha bisogno, come base, di VirtualBox o VMWare, io uso VirtualBox e va benissimo. Ma non è vagrant l'obiettivo di questo post...)

Nel mio caso volevo una ubuntu virtuale per cui ho fatto

$ vagrant init bento/ubuntu-18.04
$ vagrant up

dopo qualche tempo (tempo di scaricamento dell'immagine, se non esiste già sul PC, e avvio della macchina virtuale) avrete possiamo entrare nella macchina

$ vagrant ssh

Da dentro la sessione ssh scarichiamo e compiliamo i sorgenti di lua

    curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
    tar zxf lua-5.3.5.tar.gz
    cd lua-5.3.5
    make linux test
    ./src/lua -v
    sudo make install

l'ultimo passo è una comodità che possiamo permetterci tanto più che abbiamo una macchina virtuale tutta per lua

Proviamo il primo script lua, salviamo un file fac.lua con il seguente contenuto

function factorial(n)
    if n == 0 then
        return 1
    end
        return n * factorial(n - 1)
end

print(factorial(4))

e lo eseguiamo con:

    lua fac.lua

Ok, ora possiamo passare a qualcosa di un po' più complesso: lanciare lua da C++ e lanciare funzioni C++ da lua

Qui corro un pochino nelle spiegazioni. Se vi interessa qualcosa scrivetelo pure nei commenti. Se posso aiutarvi, volentieri

Prima di tutto ci serve un sorgente C++ che da un lato incorpori l'interprete lua e dall'altro esponga una funzione che poi lua utilizerà. Ecco qui:

#include <assert .h="">
#include <iostream>
#include <vector>
#include <thread>
#include <unistd .h="">

#include "lua.hpp"

static const char* PROMPT = "> ";
static const int num_of_threads = 10;
std::vector threads;


void call_from_thread() {
    std::cout << "Hello, World!" << std::endl;
}


static int lua_sleep(lua_State* L) {
    int isnum;
    int m = lua_tointegerx(L, 1, &isnum);
    assert(isnum);
    usleep(m * 1000);
    return 0;
}


static void add_functions(lua_State* L) {
    lua_pushcfunction(L, lua_sleep);
    lua_setglobal(L, "sleep");
}


int main(void) {
    threads.push_back(std::thread(call_from_thread));

    for (std::thread& t : threads)
        t.join();

 std::cout << "Lua shell 0.0.1" << std::endl;
 lua_State *L = luaL_newstate();  // opens Lua
 luaL_openlibs(L);  // opens the standard libraries
    add_functions(L);  // add the private library

 std::string line;
 int error;
 std::cout << PROMPT;

 while (std::getline(std::cin, line)) {
  error = luaL_loadstring(L, line.c_str());
  if (not error) {
   error = lua_pcall(L, 0, 0, 0);
  }
  if (error) {
   std::cerr << lua_tostring(L, -1) << std::endl;
   lua_pop(L, 1);  // pop error message from stack
  }
  std::cout << PROMPT;
 }

 std::cout << std::endl;

 lua_close(L);
 return 0;
}

Poi ci serve un Makefile per poter generare l'eseguibile (è un po' naive...):

shell: shell.o
    g++ -o shell shell.o -llua -lm -ldl -pthread

shell.o: shell.cpp
    g++ -std=c++11 -c shell.cpp

test: shell
    python tests.py

clean:
    rm shell *.o

Infine ci servono dei test case python che verifichino il tutto, salviamo il seguente file tests.py:

import subprocess
import unittest

class Shell:
    def __init__(self):
        self.shell = subprocess.Popen(
                ["./shell"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    def __getattr__(self, item):
        return getattr(self.shell, item)


class Test(unittest.TestCase):
    def test_print(self):
        shell = Shell()
        shell.stdin.write("print(\"ciao\")")
        stdoutdata, stderrdata = shell.communicate()
        self.assertEqual(shell.returncode, 0)
        self.assertIn("ciao", stdoutdata)
        self.assertFalse(stderrdata)

    def test_print_a_variable_value(self):
        shell = Shell()
        shell.stdin.write(
                "a = 10\n"
                "print(a)\n")
        stdoutdata, stderrdata = shell.communicate()
        self.assertEqual(shell.returncode, 0)
        self.assertIn("10", stdoutdata)
        self.assertFalse(stderrdata)

    def test_call_a_non_existent_function(self):
        shell = Shell()
        shell.stdin.write("_sleep(10)\n")
        stdoutdata, stderrdata = shell.communicate()
        self.assertEqual(shell.returncode, 0)
        self.assertTrue(stderrdata)

    def test_call_a_private_function(self):
        shell = Shell()
        shell.stdin.write(
                "a = sleep(10)\n"
                "print(\"return value: \")\n"
                "print(a)\n")
        stdoutdata, stderrdata = shell.communicate()
        self.assertEqual(shell.returncode, 0)
        self.assertFalse(stderrdata)
        self.assertIn("return value: \n> nil", stdoutdata)


    def test_call_with_wrong_argument_type(self):
        shell = Shell()
        shell.stdin.write("sleep(\"wrongtype\")")
        stdoutdata, stderrdata = shell.communicate()
        self.assertNotEqual(shell.returncode, 0)
        self.assertIn("Assertion", stderrdata)


if __name__ == '__main__':
    unittest.main()

Eh, già, nel caso ve lo stiate chiedendo, vi confermo che sono un po' appassionato sia di python che di testcase. Anzi, se devo dire la verità il mio percorso di sviluppo precedente è stato più o meno l'opposto di quello che vi ho scritto prima: sono arrivato ad una shell.cpp super-minimale con il relativo super-minimale Makefile, a quel punto ho scritto via via i test in python ed ho via via esteso lo shell.cpp per aggiungere funzionalità. Quello che trovate sopra è il risultato finale dei miei esperimenti, il mio percorso però è stato molto più "test-driven". Ma anche il TDD non c'entra con questo post per cui proseguiamo:

Ora che abbiamo i 3 file precedenti basta compilare la shell e lanciare i test (vi ricordo che siamo sempre dentro la macchina virtuale vagrant, se siete usciti basta un "vagrant ssh" per rientrare)

    make
    python tests.py

Et, voilà!

Nulla da dire, lua si integra in c++ in modo semplice e pulito e la funzione lua_sleep, per quanto idiota, rimane indubbiamente una funzione esposta da C++ e lanciata da lua.

Quello che ci rimane da fare, prossimamente perché ora si svegliano i figli e non ho più voglia di stare al PC :-), è di fare una cosa simile con python e micropython.

In passato CPython (anche grazie a "boost" e , in particolare, "boost-python") eravamo riusciti ad integrarlo liscio come l'olio ed era stato possibile esporre a python non solo funzioni bensì anche intere classi
... con micropython? vedremo!

Se la macchina virtuale non vi serve più per il momento uscite e date il comando

$ vagrant halt

A quel punto, se volete proprio buttarla via

$ vagrant destroy

Nessun commento: