sabato 11 agosto 2018

Python vs Lua (4)

Ok, ho messo un po' in ordine i miei esperimenti con micropython ed ho raccolto il tutto in un repo su github. Ecco il link

https://github.com/depaolim/micropython_vs_lua

Ricapitolo il mio obiettivo:

  1. costruire un eseguibile C++ che contenga un interprete rispettivamente python e lua (fase di "embedding")
  2. esporre delle funzioni C++ che possano essere chiamate dall'interprete (fase di "extending")

L'esperimento preliminare è sostanzialmente riuscito, per cui provo a condividere qui alcune considerazioni sull'esperienza.
Premetto che non ho ancora fatto alcuna prova sull'effettivo utilizzo di memoria, sulla efficienza di elaborazione e neppure, cosa più importante, non ho ancora provato a far girare il tutto su una schedina hardware effettiva bensì mi sono limitato a far girare il tutto su PC linux
L'ultima avvertenza preliminare è che sia per micropython che per lua non li ho utilizzati in modo esteso.
Dopo tutte queste avvertenze veniamo ora ad una bella considerazione netta:

L'embedding di micropython è un pochino più complesso di quello di Lua. In compenso l'efficienza e la configurabilità di micropython mi paiono nettamente superiori.

Nessuna sorpresa: entrambi i comportamenti sono conseguenza di scelte di design ben precise e perfettamente giustificare per l'obiettivo che ciascuno dei due si pone.

Lua fa una scelta molto semplice e "pulita": localizza tutto lo stato di esecuzione (compresi i puntatori alle funzioni globali) in una struttura denominata Stack.
Per cui, come si può vedere nel file shell_lua.cpp tutta l'inizializzazione risulta concettualmente molto essenziale.
Utilizzando però in modo pervasivo lo Stack Lua ha i seguenti svantaggi:

  1. introduce sistematicamente un ulteriore livello di indirezione in tutti gli accessi alla memoria
  2. consente un controllo pressoché nullo sulle modalità di allocazione e gestione della memoria

La scelta di micropython è quasi opposta. Lo si nota analizzando il file shell_upy.cpp dove si vede chiaramente: allocazione esplicita dello heap, dello stack e, perfino, del garbage-collector: insomma varie istruzioni che "maneggiano" oggetti a basso livello solo per inizializzare e configurare il run-time dell'interprete. Tra l'altro la maggior parte di queste strutture risultano essere statiche e globali.
Ne risulta indubbiamente una maggiore complessità per un programmatore che voglia fare l'embedding di micropython rispetto a Lua
Ma ci sono motivi ben precisi di efficienza per l'uso di queste strutture statiche e il fatto che tutto ciò venga inizializzato esplicitamente è, in effetti uno dei punti di forza delle scelte architetturali di micropython: ognuna di queste configurazioni è sotto il pieno controllo dello sviluppatore!

Non dimentichiamo che micropython ha l'esigenza primaria di poter girare su schedine a microcontrollore per cui l'efficienza, in particolare nell'accesso alla memoria, è un imperativo fondamentale
A partire dal progetto su github (rilasciato con licenza MIT, per altro) ognuno può costruire un micropython ritagliato perfettamente "su misura" sulle caratteristiche dell'hardware su cui dovrà girare.
Solo per citare alcune delle moltissime possibili scelte di embedding posso costruire un micropython: che supporta o non supporta i thread, che supporta o meno le primitive matematiche, che gira senza garbage-collector o con un gc custom, che alloca lo stack sull'uno o l'altro supporto di memoria della schedina, etc. etc. In generale micropython non da nulla per scontato, neppure la disponibilità di un filesystem: per cui prevede di poter configurare il meccanismo di import dei moduli di python andando via via a reperire i moduli sui vari supporti di memoria (Tra l'altro ho notato, disseminati nel codice, vari "punti di misurazione" per controllare, passo per passo, ad esempio, l'occupazione di memoria).

La cosa straordinaria è che tutta questa configurabilità è comunque a fronte della messa a disposizione di un linguaggio decisamente esteso ed efficace come è python.
Insomma mi pare che la sfida di Damien George di quasi 5 anni fa sia stata sostanzialmente vinta: l'intero linguaggio python disponibile per girare su schedine ridottissime.
Damien inoltre mi risulta pienamente attivo sul progetto github coadiuvato da alcuni collaboratori che si sono via via aggiunti negli anni

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

Python vs Lua (2)

Nel post precedente abbiamo parlato di micropython e ci siamo lasciati con un micropython compilato per PC.
Oggi scopriamo che micropython, pur essendo tarato per girare su hardware ridottissimo, ha a disposizione anche funzionalità che non ti aspetteresti.
Ne vediamo, ad esempio due:
  • un sistema di gestione pacchetti tipo pip
  • e il modulo standard per i thread
Useremo entrambi per fare delle prove con mqtt

Partiamo dai thread

Micropython, battery-included non ha il tradizionale modulo standard multithreading bensì, al momento, ha solo il modulo base _thread con le API di basso livello
(uhm, chissà se il modulo _thread esiste su tutte le piattaforme supportate da micropython, io sono su linux: c'è posix sotto, è comunque una situazione favorita, sulle altre piattaforme sarà da verificare)
Comunque, anche se abbiamo solo _thread possiamo già fare qualche bel giochino

Prima di tutto, per non dover lavorare con _thread, definiamoci un modulo threading "hand-made" che che esponga l'API standard

Ecco qui:

import _thread
get_ident = _thread.get_ident

class Thread:

    def __init__(self, group=None, target=None, name=None, args=(), kwargs=None):
        self.target = target
        self.args = args
        self.kwargs = {} if kwargs is None else kwargs

    def start(self):
        self.running = _thread.allocate_lock()
        self.running.acquire()
        _thread.start_new_thread(self.run, ())

    def join(self):
        assert self.running.acquire()

    def run(self):
        self.target(*self.args, **self.kwargs)
        self.running.release()

salvandolo su un file con nome threading.py potremo banalmente utilizzarlo come se avessimo a disposizione effettivamente threading
(L'implementazione è molto banale e naive, mi sono ispirato ai sorgenti standard riducendoli drasticamente)

A questo punto siamo in grado di costruire il nostro primo test con i thread
(Notare che il modulo utime potrebbe non esserci su tutte le piattaforme oppure alcune piattaforme potrebbero disporre di moduli specifici esempio per la esp8266)

import utime
import threading

def target(name, count):
    for idx in range(count):
        utime.sleep(0.5)
        print("I'm {} - {}".format(name, threading.get_ident()))

def main():
    print("begin...")
    t = threading.Thread(target=target, args=("THREAD", 10))
    t.start()
    target("MAIN", 3)
    t.join()
    print("end.")

if __name__ == '__main__':
    main()

Salviamo con nome first_thread_test.py e lanciamo con micropython first_thread_test.py

Et, voilà! Abbiamo due thread che scrivono in modo un po' confuso sullo standard output
E' ora di alcune osservazioni conclusive

micropython funziona bene. In meno di 400K di eseguibile abbiamo a disposizione la grandissima parte del linguaggio (se ci pensate abbiamo utilizzato funzioni, classi, import di moduli...) ed una nutrita schiera di moduli della libreria standard.
L'esempio con i thread è uno dei più "cattivi" perché utilizza primitive di sistema operativo abbastanza specifiche. Nel nostro caso eravamo su un sistema posix e abbiamo potuto raggiungere egregiamente l'obiettivo

So far so good!

Vi avevo promesso prove con mqtt... e con Lua!... yes! Stay tuned! ;-)

Per non lasciarci con troppe cose in sospeso eccovi una chicca: micropython, per quanto ridotto ai minimi termini, dispone di un sistema di gestione dei pacchetti compatibile con pip.
E su PyPI ci sono tanti moduli specifici per micropython

Ecco ad esempio come installare un modulo ed utilizzarlo:

./micropython -m upip install micropython-pystone

(Il modulo, di default, finisce in $HOME/.micropython/lib)

L'esempio completo è qui

Lua vs Python

Stiamo valutando le opzioni disponibili per inserire un linguaggio di "scripting" in un sistema scritto in C.
I moduli C possono trovarsi a girare su un PC con sistema operativo, su sistemi embedded o, al limite, perfino su microcontrollori.
La prima ipotesi che viene in mente, da pythonista, è quella di micropython.
https://micropython.org/

Dando un occhiata al progetto su github scopro che si tratta di un progetto interessante e in piena attività
https://github.com/micropython/micropython/graphs/code-frequency

E' vero che, come al solito, pochi contributor fanno la gran parte dei commit
https://github.com/micropython/micropython/graphs/contributors

Però anche il fatto che github, ad oggi, conti ben 269 contributor suona bene

Ok, Allora proviamolo!

Senza la pretesa di farlo girare su schedine dedicate, come primo passo, mi accontento di compilarlo per PC

L'opzione è perfettamente prevista (come tanti altri porting, perfino per Windows) e funziona. Ecco i, molto semplici passi

Io per prima cosa ho preferito creare un virtualenv con python 3.6.6, per non dipendere dal python2 di sistema. Si tratta di un passo opzionale. E comunque nel README dice che richiede python (at least 2.7 or 3.3)

Gli altri passi, per Ubuntu, sono riportati qui:
https://github.com/micropython/micropython/wiki/Getting-Started#debian-ubuntu-mint-and-variants

git clone https://github.com/micropython/micropython.git
cd ports/unix
make axtls
make

Et voilà, abbiamo micropython! Basta un bel ./micropython e potete iniziare a dare i primi comandi

Ok, per il momento ci fermiamo qui, stay tuned!
... ma il titolo non parlava anche di Lua? Yes, stay tuned ;-)