Un contributo sulle funzioni esterne in Gambas a cura di Doriano B.

Da Gambas-it.org - Wikipedia.

Questo documento è stato scritto originariamente in inglese da Doriano B. (traduzione da parte della Comunità di gambas-it.org).


Interfacciare Gambas con librerie esterne

INTRODUZIONE

Ci sono un sacco di librerie condivise disponibili in un sistema Linux, capaci di far un sacco di cose utili ed un sacco di queste librerie possono essere usate in Gambas usando alcune delle sue funzionalità.

Il primo passo da effettuare è quello di cercare una libreria adatta ai nostri scopi; non tutte le librerie possono essere usate ma la maggioranza si. Il prerequisito è che la libreria sia scritta in C. Molte librerie sono scritte in C, altre in C++ ed altre ancora in altri linguaggi. Questo documento è focalizzato su librerie scritte in C.

Una volta che si è trovata la giusta libreria bisogna comprenderne la logica di funzionamento per capire cosa è necessario e cosa deve essere usato per il funzionamento del progetto finale. Tutta la documentazione della libreria, ed alcuni possibili esempi, dovrebbero essere letti con attenzione. Librerie non sono programmi e perciò la loro filosofia cambia sensibilmente. Un programma tende a contenere solo le subroutine utili al proprio funzionamento mentre una libreria, come lo stesso nome indica, contiene una raccolta di subroutine da utilizzare più volte da più programmi. Non è raro trovare nelle librerie due routine che svolgono lo stesso lavoro ma in maniera differente. Le librerie spesso sono scritte tenendo a mente che esse dovranno essere usata da altri linguaggi diversi dal C: python, ruby, ocaml e molti altri, Gambas compreso. Le librerie tendono ad incapsulare i dettagli all'interno di un "handle" im maniera simile agli oggetti di Gambas; ma essi non hanno proprietà e metodi - tutto viene caricato alla lettura della funzione che tali handles passano. Se si pensa ad una classe di Gambas contenente tre proprietà e quattro metodi, una libreria esterne implementerebbe la stessa cosa con sette (tre + quattro) subroutine e probabilmente una coppia in più per la creazione e distruzione dell'oggetto. A parte ciò non c'è una grossa differenza tra l'impostazione di una proprietà e la chiamata di una funzione. La seconda è solo un pò più lunga da scrivere. Per cercare ciò di cui abbiamo bisogno dobbiamo essere pronti a leggere un sacco di documentazione molto spesso scritta male (hey, a proposito, quanto è buona la documentazione del nostro software?).

La Dichiarazione esterna

La dichiarazione esterna è semplice. Essa è come la dichiarazione di una normale subroutine di Gambas ma preceduta da EXTERN. La parola chiave EXTERN dice a Gambas che il cuore della procedura non è definito nel programma che stiamo scrivendo ma da qualche altra parte (una libreria esterna).

Libreria da usare

Dobbiamo anche precisare quale libreria vogliamo usare: questo viene fatto dalla clausola "IN libraryXXX" In alternativa, è possibile utilizzare una dichiarazione distinta per LIBRARY: tutte le successive dichiarazioni extern faranno riferimento a questa dichiarazione. Per ciascuna "IN library" o "LIBRARY xxx" è possibile specificare un numero di versione dopo i due punti e questo è raccomandato.

Vediamo un esempio, scelto per la sua semplicità:

LIBRARY "libc:6"

EXTERN getgid() AS Integer 

Queste due righe dicono che una funzione denominata "getgid" esiste nella libreria "libc" versione 6 Questa funzione non accetta alcun parametro e restituisce un intero (il gruppo ID).

La stessa cosa può essere scritta così:

EXTERN getgid() AS Integer IN "libc:6" 

Vediamo un altro esempio, un po' più complicato. Questa volta abbiamo parametri, è l'ultima cosa da inserire nella dichiarazione formale:

' int kill(pid_t pid, int sig);
EXTERN killme(pid AS Integer, sig AS Integer) AS Integer IN "libc:6" EXEC "kill " 

La prima riga (il commento) mostra la dichiarazione originale, e la seconda linea il codice gambas. Si può notare un certo numero di cose. In primo luogo, che cosa significa la dichiarazione originale? Essa significa "C'è un funzione di nome kill che restituisce un int e accetta due parametri, il primo è chiamato pid e il suo tipo è pid_t; il secondo è chiamato sig e il suo tipo è int. Contrariamente a Gambas, il linguaggio C mette il tipo di una variabile prima della variabile anziché dopo, In secondo luogo. ciò che in Gambas è chiamato "Integer", in C si chiama "int". In terzo luogo, che cos'è pid_t? è un "type", possiamo capirlo dal fatto che è posizionato dove ci aspettiamo un identificatore di tipo e dal fatto che termina con "_t" (undescore t). In quarto luogo, una nuova clausola EXEC "kill " è utilizzata nella dichiarazione di Gambas. Questo è necessario perché vogliamo utilizzare una funzione denominata kill, ma KILL è un nome già utilizzato da Gambas. Così, in Gambas, dobbiamo utilizzare un nome di funzione diverso, ma comunque dobbiamo indicare il suo vero nome all' interno della libreria. La dichiarazione dice: "Dichiaro una funzione esterna denominata "killme", ma il suo vero nome è "kill". Ho scelto il nome killme perché negli esempi gambas questa funzione è utilizzata per terminare il programma in esecuzione.

Ad essere sincero ho notato che, anche senza rinominare la funzione da "kill" a "killme", il programma funziona lo stesso. Può essere che questo abbia qualcosa a che fare con l'uso di maiuscole e minuscole. Il C è case-sensitive ma Gambas no e quindi non c'è differenza tra kill (minuscolo) e KILL (maiuscolo). In ogni caso, quando c'è un conflitto di nomi possibili, è meglio usare questa ridenominazione tecnica.

ATTENZIONE --- fine della parte semplice

(non Scherzo)

A questo punto occorre ricordare alcune cose. La maggior parte delle librerie adatte sono scritte in C, che è un linguaggio diverso da Gambas. Avremo bisogno di conoscere almeno un po' le dichiarazioni del C, al fine di tradurle in Gambas. Riferendosi all'ultimo esempio, ci si potrebbe chiedere perché ho tradotto il type "pid_t" come Integer. La risposta più semplice e corretta è "perché nel mio sistema il type pid_t è in realtà un intero". Questa risposta è davvero corretta, ma deve essere spiegata meglio, parlando di agrumi(?). Siamo in grado di pensare a limoni e arance, entrambi sono agrumi e sono molto simili: hanno più o meno lo stesso peso, e spesso sono intercambiabili; si possono mangiare direttamente, o spremerli e bere il loro succo, ma è improbabile che si metterà succo d'arancia sul pesce fritto. In C, questo è espresso dal fatto che è improbabile che si desidera utilizzare la funzione kill() passandogli un intero arbitrario. Sicuramente, si passa un identificatore di processo (PID), che è in realtà un intero, ma è presumibilmente più corretto indicarlo come pid_t. Nella mia motivazione, ho anche detto che "sul mio sistema il type pid_t....." Sì, sul mio sistema, e sulla maggior parte dei sistemi, il tipo pid_t è un intero, ma questo potrebbe anche essere diverso su altri sistemi. La risposta definitiva può essere individuata digitando questi due comandi in un terminale:

grep -r pid_t /usr/include/* |grep "#define" grep -r pid_t /usr/include/* |grep typedef 

che mostrerà il modo in cui i types sono gestiti in C. Questo argomento è troppo complicato per andare oltre, dato che Gambas gira su Linux, e presumibilmente su sistemi desktop, si può considerare che tutti i parametri passati a una funzione, e quelli ritornati da essa, saranno un intero, o puntatori - che sono interi anche loro, oppure stringhe - che sono a loro volta puntatori che sono interi. Ci possono essere anche numeri in virgola mobile - float e double. La seguente tabella elenca alcuni dei tipi che si possono incontrare in una dichiarazione in C, e il tipo adatto da utilizzare in Gambas:

int -> integer
long -> long
float -> single
double -> float
xxxx* -> pointer (L'asterisco significa esattamente "puntatore")
char* -> pointer

I puntatori

Inizieremo ad introdurre brevemente i puntatori, che sono poco utilizzati in Gambas. Un puntatore è un intero, ma utilizzato in maniera diversa. La cosa che somiglia più ad un puntatore in Gambas è un'istanza di classe. Quando si crea, per esempio, un form in Gambas, un sacco di dati sono archiviati da qualche parte in memoria. Quella memoria manterrà tutte le impostazioni specifiche del form: il suo titolo, il suo colore, l'elenco di tutti i suoi children, e così via. L'indirizzo di tale blocco di memoria viene restituita al programma e memorizzato nella variabile che si riferisce al form appena creato.

MainForm = NEW Form() 

La variabile "MainForm" è in realtà un puntatore: in soli 4 (o 8) byte vengono tracciati un sacco di dati, memorizzati da qualche parte nella memoria in una specifica locazione (indirizzo). La memoria è una lunga sequenza di celle (byte), ciascuna identificato da un numero progressivo. Un puntatore contiene il numero di identificazione di una cellula di memoria (il suo indirizzo) In C, i puntatori sono utilizzati per due ragioni: la prima è che passare solo un indirizzo (un puntatore contiene un indirizzo), è molto più veloce che passare un sacco di dati, questa è la ragione stessa per cui in Gambas le variabili di istanza di classe come "MainForm" sono simili ai puntatori. Il secondo motivo è che la funzione chiamata dovrebbe modificare la variabile che abbiamo passato nella chiamata. Per esempio, se noi in Gambas scriviamo:

INPUT a ' dove "a" è un integer 

in C si può scrivere:

void input(int *a); ... input(&a); 

La ragione è che vogliamo che il nostro comando INPUT riempia la nostra variabile "a". In C, dobbiamo chiamare input() e dirli dove si trova la nostra variabile per poter riempire la variabile. La e commerciale "&" prende l'indirizzo della variabile e la passa alla funzione. La dichiarazione di input() dice: "int *a", che afferma che "a" non è un numero intero, ma un puntatore ad un intero, cioè il parametro dice dove trovare il valore, non il valore stesso.

IMPLEMENTAZIONE DEI PUNTATORI CON GAMBAS

Gambas ha il tipo di dato "puntatore" ed una serie di operazioni adatte ad essi. Per usare un puntatore, è richiesta una normale dichiarazione come per qualsiasi altra variabile. Poi, un valore deve essergli assegnato. Quando si utilizza una normale variabile, spesso è possibile assegnare un valore letterale, ad esempio, si può scrivere "a = 3". Con i puntatori questo non è consigliabile. Un puntatore dà accesso a qualsiasi locazione di memoria, ma si dovrebbe sapere in anticipo la locazione che ci interessa, e che la posizione sia quella corretta per gli scopi previsti, altrimenti Gambas o il sistema operativo si arrabbia. Questo più o meno equivale a dire che non si può scrivere "MainForm = 3". È possibile scrivere:

"MainForm = NEW Form()", o "MainForm = UnAltroQualsiasiForm", o "MainForm = NULL ".

Quindi, un'assegnazione diretta ad un puntatore sarà sempre o NULL, o un altro puntatore, o per una chiamata a qualche funzione che restituisce un puntatore. Proprio come per una variabile di istanza di classe come ad esempio MainForm.

A volte una funzione esterna restituisce un puntatore e questo puntatore sarà necessario per invocare le chiamate successive alla libreria esterna. Questo caso è molto simile alla creazione di un form e all'uso dei suoi riferimenti per operare sul form stesso. In questo caso il dato dietro (puntato) al puntatore è detto "opaco": non si può guardare oltre una cosa opaca, così noi non sappiamo e non vogliamo sapere. Questo è il caso più semplice; un esempio su tale situazione è l'uso della libreria LDAP. La prima cosa da fare per interagire con LDAP e aprire una connessione con il server. Tutte le operazioni successive saranno effettuate sulla connessione creata da una specifica chiamata. Le cose stanno più o meno così:

LIBRARY "libldap:2"
PRIVATE EXTERN ldap_init(host AS String, port AS Integer) AS Pointer 
  
  PRIVATE ldapconn as Pointer
  ...
  ldapconn = ldap_init(host, 389) 
  IF ldapconn = NULL THEN error.Raise("Can not connect to the ldap server")

Come si vede LIBRARY è specificato. Così una funzione EXTERN è dichiarata. Questa funzione è l'unica che può essere chiamata per effettuare qualche operazione con LDAP. Le ultime due righe sono le uniche che possono aprire la connessone e archiviare i propri handle, o istanze, per questa connessione. In questo caso specifico, ldap_init() restituisce NULL se qualcosa non va per il verso giusto così possiamo testare la presenza del valore NULL per restituire l'errore. Una volta ottenuto l'handle alla connessione, esso deve essere specificato ad ogni successiva chiamata ala libreria LDAP. Ad esempio per eliminare una voce dal database si può utilizzare il seguente codice:

PRIVATE EXTERN ldap_delete_s(ldconn AS Pointer, dn AS String) AS Integer
... 

PUBLIC SUB remove(dn AS String) AS Integer 
  DIM res AS Integer 
  res = ldap_delete_s(ldapconn, dn)
  ...

Sfortunatamente le cose non sono sempre così semplici. Una delle ragioni per cui il C utilizza i puntatori è la possibilità delle subroutine di scrivere alcuni dati nelle locazioni indicate dai parametri passati. Rimanendo sull'inizializzazione di una libreria, ALSA è diversa.

Iniziare un dialogo con ALSA

Per iniziare un dialogo con il sequencer di ALSA è necessario un handle con il sequencer stesso. La dichiarazione in C di tale funzione è:

int snd_seq_open(snd_seq_t **seqp, const char * name, Int streams, Int mode);

Aiuto!Cos'è questo "snd_seq_t **seqp" ? Noi sappiamo che gli asterischi sono utilizzati per indicare un puntatore - perciò cosa può significa un doppio asterisco? È facile: un puntatore a puntatore. Questa funzione snd_seq_open() usa il puntatore per riempire un valore; questo valore è esso stesso un puntatore. A differenza del caso di LDAP, dove la funzione ldap_init() restituisce un valore, qui questa funzione ne restituisce due. Il valore ritornato dalla funzione è un errore - tutte le funzioni ALSA seguono questo schema. Un valore di ritorno pari a zero significa che il codice ha avuto successo. Così per restituire più di un valore la funzione può solo scrivere alcuni dati in alcune locazioni che noi specifichiamo usando un puntatore. Il valore che la funzione scrive è di tipo puntatore perciò è usata la notazione del "doppio puntatore". Fin qui tutto bene. Ma possiamo tradurre ciò in Gambas? Si e no. Abbiamo bisogno di un puntatore è questo non è un problema. Allora noi dobbiamo avere l'indirizzo di questo puntatore per ottenere un puntatore a puntatore. Gambas 3 può fare ciò ma Gambas 2 no. Guardiamo l'esempio applicabile solo a Gambas 3. La funzione VarPtr() restituisce l'indirizzo in memoria di una variabile o, in altre parole, il puntatore alla variabile stessa - è ciò che dicelo stesso nome: VAR-PTR, "Variable pointer" ("puntatore alla variabile"). In Gambas 3 scriveremo:

PRIVATE EXTERN snd_seq_open(Pseq AS Pointer, name AS String, streams AS Integer , mode AS Integer) AS Integer 
 ... 
 PRIVATE AlsaHandler as Pointer 
 ... 
 err = snd_seq_open(VarPtr(AlsaHandler), "default", 0, 0)

la dichiarazione EXTERN dice che that snd_seq_open()si aspetta un puntatore, che fondamentalmente è vero: snd_seq_open() si aspetta un puntatore a puntatore che è in ogni caso un puntatore. Così noi dedichiamo una variabile Alsahandler come puntatore e li passiamo l'indirizzo mediante VarPtr() che ritorna un puntatore alla variabile. In Gambas 2 ciò non è possibile - non abbiamo VarPtr(). Dobbiamo in ogni modo dichiarare una variabile per contenere l'handle, come prima, ma poi non possiamo avere il suo indirizzo o un puntatore ad esso. Possiamo risolvere il problema utilizzando un'altra strada. Dobbiamo trovare una locazione in memoria da passare ad ALSA e, fatto ciò, andare a prelevarla dala sua locazione. In Gambas 2 l'unico modo è utilizzare la funzione Alloc(). Usando Alloc() riserviamo , da qualche parte, una zona di memoria ed otteniamo il suo indirizzo. Esso è ciò di cui abbiamo bisogno per passare a snd_seq_open() un puntatore contenente un indirizzo. Bene, partiamo a scrivere qualcosa?Ovvio.

'int snd_seq_open(snd_seq_t **seqp, const char * name, Int streams, Int mode); 
PRIVATE EXTERN snd_seq_open(Pseq AS Pointer, name AS String, streams AS Integer , mode AS Integer) AS Integer 

PRIVATE AlsaHandler as Pointer ... 
DIM err AS Integer 
DIM ret AS Pointer 
ret = Alloc(4) ' 4 is the size of a pointer in 32-bit systems; 8 for 64-bit systems 
err = snd_seq_open(ret, "default", 0, 0)

Quando vogliamo aprire una connessione ed ottenere un handler, ci riserviamo un po' di memoria e passiamo il suo indirizzo al fine di ottenere dati utili per snd_seq_open(). Ma poi, come possiamo leggere questa locazione per recuperare l'handler? Qui arriva la funzionalità dei puntatori in Gambas. I puntatori possono lavorare come flussi - puoi leggerli o scriverli. Attualmente la memoria del computer è un file composto da celle di memoria, giusto? Possiamo leggere il valore di un puntatore con:

READ #ret, AlsaHandler

A questo punto ci siamo riusciti. È un pò come un'Odissea ma ne è valsa la pena. Dobbiamo solo rilasciare la memoria che abbiamo allocato con Alloc() così l'Odissea non è ancora finita. Questa memoria è stat utilizzata in maniera temporanea ma se tale operazione è stata effettuata tante e tante volte nel corse del programma, esso avrà occupato un sacco di memoria. Normalmente Gambas ha un sistema di gestione della memoria automatico che in questo caso non può aiutarci poiché non conosce cosa si sta facendo con la memoria è perciò siamo responsabili di liberarla quando l'abbiamo usata.

Free(ret)

Ci sono altri motivi per cui si pyò usare un puntatore. Prendete la dichiarazione di getloadavg(), un ottima funzione che ci dice quanta della nostra CPU è stata occupata nell'ultimo minuto.

int getloadavg(double loadavg[], int nelem);

Questo linguaggio C può passare anche array di funzioni? Certo. E provate a immagine come lo fa? Con i puntatori di nuovo. In questo caso l'array passato alla funzione deve essere riempito con uno o più valori. Ognuno indica un diverso tipo di carico medio; ogni valore sarà inserito in una locazione consecutiva nell'array. Il C non è abbastanza intelligente quanto è grande un array così la funzione non può sapere quanti valori ci sono scritti all'interno. Noi dobbiamo dire ciò alla funzione mediante il parametro "nelem". Per farla breve la corretta dichiarazione per tale situazione è:

EXTERN getloadavg(ploadavg AS Pointer, nelem AS Integer) AS Integer

Abbiamo bisogno di passare un puntatore perché la funzione getloadavg se lo aspetta anche se ciò non era facilmente notabile dalla sua dichiarazione. Il puntatore deve puntatore a della memoria libera poichè la funzione riempirà tale area di memoria. Fatto ciò dobbiamo leggere il valore e poi liberare la memoria. Un esempio del suo uso è:

PUBLIC SUB get_load() AS Float

  DIM p AS Pointer
  DIM r AS Float

  p = Alloc( 8 ) 

  IF getloadavg(p, 1) <> 1 THEN
    Free(p)
    RETURN -1   ' error
  ENDIF READ #p, r 
  Free(p)
  RETURN r

END

La subroutine è semplice. Si assegnano 8 bytes perché in Gambas un float occupa 8 bytes. Fatto ciò chiamiamo getloadavg() che si occuperà di riempire questi 8 bytes. Se l'operazione a successo o eno bisogna libera la memoria allocata. Ma se l'operazione ha successo dobbiamo prima leggere la memoria. Questo spiega perchè ci sono due "free(p)" nella suboutine. Una soluzione più elegante potrebbe essere l'uso di FINALLY ma con il codice attuale siamo più vicini allo spirito del C... getloadavg() restituisce il numero di valori letti. Se richiediamo un solo valore, è giusto interpretare il ritorno di un risultato diverso da uno come un errore. Se effettuiamo la richiesta per tre valori e ne otteniamo solo due allora abbiamo avuto una strana situazione - qualcosa a metà strada tra un ottimo risultato è un fallimento. Questa e altre cose simpatiche possono essere viste quando si prova ad usare qualche interfaccia storica. Per esempio, in qualche versione di UNIX non c'è un metodo pulito per leggere il nome di un file. La funzione ritorna il numero di caratteri scritti ma non indica se il nome è più corto di tale valore. Così sei sicuro di aver letto il nome intero quando passi un buffer più lungo del risultato della funzione. Ma il risultato della funzione lo si ottiene solo dopo la chiamata non prima! Un uso tipico è quello di scegliere un valore arbitrario , tipo 256, ed usarlo come prima prova. Se esso fallisce si aggiungono altri 256 e si riprova. E via così... Torniamo al nostro getloadavg(). Abbiamo usato Alloc(8) poiché in Gambas un float occupa 8 bytes.Abbiamo usato un float in Gambas per sostituire un double in C. Ma dove si può vedere che un double occupa 8 bytes? Infatti esistono computer in cui un double occupa 10 bytes. Questo è un problema serio poiché su tali computer la subroutine non funziona. Si potrebbe allocare più memoria, forse 64 bytes invece di 8: sono sicuro che non esiste un computer che usa più di 64 bytes per un numero in virgola mobile. Se si prova a leggere un valore di 8 bytes in uno di 64 bytes esso perderebbe il suo senso. Forse è meglio lasciare che il programma vada in crash invece di far finta che funzioni. Una domanda allora sorge spontanea...come può un programma C lavorare su diverse architetture? La risposta è la seguente: una nuova architettura deve avere un set omogeneo di kernel, include e compilatore. In un vero programma C non si vedrà mai una cosa del tipo "alloc(8)" ma qualcosa come "alloc(sizeof(double))". Il compilatore conosce la dimensione del double e la parola chiave "sizeof" inserisce tale conoscenza nel codice sorgente.

Di più sui puntatori

Alcune spiegazioni migliori sono necessarie a questo punto. L'istruzione "READ #ret,..." legge qualcosa dalla locazione puntata da "ret". È importante sottolineare ancora una volta che questo genere di cose devono essere progettate con cura. Lavorare male con i puntatori è una delle più comuni cause di fallimento dei programmi in C e, nell'uso dei puntatori, Gambas non è differente. In questo caso è facile perché abbiamo fatto il nostro lavoro in poche righe. La semantica dell'istruzione READ nei puntatori assomiglia assomiglia a quella di STREAM ma con un'importante differenza: mentre uno stream viene fatto avanzare dopo la lettura o la scrittura, la stessa operazione non avviene sui puntatori. Se la memoria contiene due variabili che devono essere lette una dopo l'altra, dobbiamo far avanzare il puntatore manualmente.

READ #mypointer, var1_4byte 
mypointer += 4 
READ #mypointer, var2_4byte

Come si può notare, è possibile trattare un puntatore come un intero. Usando questo meccanismo un programmatore può camminare avanti e indietro nella memoria e simulare quello che in C è detto "struct". Una struct (struttura) in C è un gruppo di variabili eterogenee poste fianco a fianco che poi possono essere trattare come un unica variabile. La sua controparte in Gambas è la classe. Le strutture sono spesso indicate da un puntatore, specialmente quando devono essere passate ad una funzione. Vedremo dopo come implementare tale metodo alternativo in Gambas. Ma ora stiamo parlando di puntatore e chiudiamo l'argomento. Il linguaggio C ha anche le "unioni" che sono cose sconosciute a Gambas e che possono essere simulate con un puntatore (non totalmente vero). L'unione è l'insieme di una o più variabili che condividono la stessa porzione di memoria. Scrivendo in una variabile si modificano implicitamente anche le altre: sono sovrapponibili. La ragione di ciò è che si vogliono scrivere layout diversi in unico tipo di dato. Dalla combinazione di strutture e unioni è possibile ottenere layout difficili da gestire e da capire. Per fare un esempio parleremo ancora del sequencer ALSA. Il sequencer lavora con eventi (le note suonate) che hanno un "time stamp" che indica quando questi eventi sono riprodotti o caricati. Questo "time stamp" può essere espresso in ticks che normalmente è il modo di lavorare di un metronomo. Questi ticks sono Integer. MA ALSA va ben oltre, e permette di utilizzare time stamp in tempo reale, un'indicazione più precisa, utile per sincronizzare la musica con le altre cose (video, per esempio). Questo sistema di misura è più preciso ma necessita di un maggiore quantità di memoria (due Integers). Ci sono eventi che necessitano di un tempo specificato con 4 bytes e tempi che necessita di 8 bytes. Essi dovrebbero semplicemente utilizzare due campi, uno da 4 bytes ed uno da 8 bytes, uno dopo l'altro. Usando una unione si risparmiano 4 bytes. La vera memoria riservata per il time stamp è 8 byte, abbastanza grande da contenere entrambi i valori ma a livello logico i due valori si escludono a vicenda. Tutto questo è riconosciuto automaticamente dal compilatore C. Quando usiamo le unioni in Gambas dobbiamo fare tutto ciò da soli.

Una drum machine in Gambas

[per Gambas 2]
Un esempi concreto di tutto ciò che abbiamo visto sino ad ora sono le librerie ALSA: una semplice e basilare drum machine può essere implementata in Gambas. Prima di tutto, cos'è una drum machine? Essa è una macchina che emula la combinazione di un batterista e i suoi strumenti. Alcuni musicisti la usano, sopratutto coloro che scrivono da soli la propria musica. In Gambas ciò può esser fatto usando ALSA. ALSA sta per Advanced Linux Sound Architecture ed il suo compito è quello di offrire una serie completa di funzioni per produrre suoni e di conseguenza musica. Dal punto di vista del computer suoni generici e musica sono due cose differenti. Se si riproduce un file mp3, ALSA dirige l'mp3 alle casse senza conoscere o analizzare nulla. Il nostro interesse è un'altra interfaccia - l'interfaccia del sequencer. Il sequencer interagisce con "eventi" che sono "eseguiti" nel momento giusto usando parametri adeguati (o proprietà) per tale evento. Se pensiamo ad un pianista, possiamo vedere come egli prema un tasto, o più tasti insieme, in momenti differenti. In parole povere ogni pressione di un tasto è un evento. Le tre cose più importanti nel momento in cui si preme un tasto sono: 1) quando il tasto è stato premuto; 2) quale tasto è stato premuto; 3) con che forza il tasto è stato premuto. Se si vogliono premere due tasti nello stesso momento allora si devono creare due eventi con lo stesso "timestamp". Se si vuole suonare un accordo allora si devono creare tre eventi aventi lo stesso tempo, tre note differenti e (probabilmente) la stessa intensità. Fatto ciò si inviano questi eventi al sequencer che si preoccuperà di inviarli ad un qualcosa che possa riprodurli. Il sequencer non è in grado di riprodurre suoni: essi vengono prodotti da altri software o mediante un interfaccia MIDI che si rivolge ad uno strumento esterno. Se invece di un pianoforte viene detto "voglio le trombe" allora le tre note verranno eseguite da tre trombe. Quest'interfaccia non indica quanto deve durare una nota ma essa viene eseguita finché non interviene un evento che la ferma. Perciò una singola nota è attualmente composta da due eventi: un NOTE-ON ed un NOTE-OFF. Nel caso della batteria, una nota identifica una differente percussione: bass drum, snare drum, cymbals, maracas, bells e molto altro ancora. Musica e computer hanno molto in comune. Ad esempio un tipico metro musicale è composto da 4 quarti. Non è comune il numero quattro nei computer? I tasti del piano sono numerati e la forze (velocità) con cui essi sono premuti è espressa mediante un numero così come anche la sua durata. Ci sono altri valori importanti, ad esempi la forza che un flautista usa per soffiare nel proprio strumento (dopo l'inizio della nota), ma non andremo così affondo. Per lasciatemi dire che un ottimo sequencer combinato con un ottimo hardware può simulare in maniera sorprendente un'intera orchestra. Una semplice drum machine è composta da una griglia dove ogni cella rappresenta una nota: il numero di righe rappresenta il numero di note che essa può riprodurre (in una drum machine ogni nota rappresenta una percussione o uno strumento). Le colonna della griglia, invece, rappresentano il momento in cui queste note devono essere riprodotte. La griglia contiene due misure per la riproduzione - questo dovrebbe garantire un ritmo normale. Ogni misura è divisa in 4 quarti ed ogni quarto è diviso in 4 sedicesimi. La riga più alta funge da righello e la colonna più a sinistra è usata per ascoltare uno strumento. Il click su una cella contente un "o" marker, per riprodurre il modello si effettua la pressione sul tasto "Riproduci griglia". Altri button producono altri suoni giusto per mostrare cose semplici come accordi, legati, arpeggi. Per avere un programma che sia in grado di produrre dei suoni, il client/device e la porta esatta (terminologia di ALSA) devono essere scritti come prime due righe di FMain.class e dipendono dall'hardware installato. Digitando il comando "aconnect -ol" nel terminale si ottengono i dispositivi a disposizione. Se un software di sintetizzazione è installato, come timidity, esso verrò mostrato probabilmente come "client 128". Il device di output MIDI dovrebbe essere il numero 14. Il numero della porta dovrebbe essere quasi sempre 0. Un altro metodo per cercare i valori corretti è quello di eseguire software come KMidi e prelevare le sue configurazioni midi. La ragione principale dell'analisi di questo software è che si vuole ottenere un interfaccia completa con una libreria esterna. La maggior parte degli argomenti sono già stati trattati ma una ancora da studiare è l'uso delle strutture C con i puntatori. Un metodo alternativo all'uso dei puntatori è quello di dichiarare una variabile in una classe passare l'istanza di tale classe alla funzione esterna. Questo sistema sarebbe sia migliore che più chiaro ma i puntatori sono più versatili. Un approccio migliore lo si ha già in Gambas 3 grazie all'uso nativo delle strutture. Poiché il programma è specifico per ALSA saltiamo tutto tranne "l'evento struttura". Una vota che si ha a disposizione tutto ciò che è richiesto da ALSA (apertura, creazione di code, porte, avvio e così via) rimane solo da costruire l'evento ed inviarlo ad ALSA che lo riprodurrà nel momento esatto.

Definizione degli eventi Midi in ALSA

Un evento è così definito da ALSA:

 snd_seq_event_type_t
   type unsigned char
   flags unsigned char
   tag unsigned char
   queue
 snd_seq_timestamp_t time
 snd_seq_addr_t source
 snd_seq_addr_t dest
  union{
   snd_seq_ev_note_t note
   snd_seq_ev_ctrl_t control
   snd_seq_ev_raw8_t raw8
   snd_seq_ev_raw32_t raw32
   snd_seq_ev_ext_t ext
   snd_seq_ev_queue_control_t queue
   snd_seq_timestamp_t time
   snd_seq_addr_t addr
   snd_seq_connect_t connect
   snd_seq_result_t result
  };

La prima linea che precede union contiene ogni tipo di evento; infatti troviamo l'evento "type", qualche "flag", un "tag", la "queue" a cui si accoda l'evento, la "source" (chi scatena l'evento?) il "dest" (a chi sarà inviato l'evento?). Si noti alla prima riga: "snd_seq_event_type_t type". il campo è chiamato "type" ed il suo tipo è "snd_seq_event_type_t type". Così se dovessimo andare nella documentazione per vedere come è fatto questo tipo troveremmo:

typedef unsigned char snd_seq_event_type_t

Questa linea sta a significa che "snd_seq_event_type_t" è un altro modo utilizzabile per indicare l'unsigned char. Un unsigned char è l'equivalente di un byte. I prossimi tre campi della struttura sono flag, tag e queue, ognuno dei quali è un unsigned char, ciascuno di un byte. Successivamente il timestap è dichiarato come "snd_seq_timestamp_t"; cercando ancora la dichiarazione troviamo che esso è un unione contenente sia un midi tick (un unsigned int) o una struttura che è composta da due unsigned int. Il risultato di ciò è che la lunghezza di questo campo è di 2 unsigned int, ovvero 8 bytes in un architettura a 32 bit. La prima parte di un evento è composta, dal suo punto di vista, dai seguenti campi: type_of_event un flags a singolo byte un tag a singolo byte una queue a singolo byte un timestamp a singolo byte, composti da:

tick (int) or tv_sec an integer
tv_nsec an integer

Se volessimo riempire il campo "tick" dovremmo indirizzare un puntatore all'inizio della memoria dell'evento, fatto ciò aumentarne il valore di 4 e dopo scrivere al puntatore il valore voluto (un Integer). La classe ALSA del C alloca memoria giusto per un evento:

PUBLIC SUB alsa_open(myname AS String)
        ...
        ...
' alloca un evento per lavorarci. è globale per evitare allocazioni/deallocazioni onerose
ev = Alloc(SIZE_OF_SEQEV)

e poi manipola questa memoria prima di passare l'evento ad ALSA. La subroutine prepareev() ripulisce l'evento e ne riempe le parti comuni. Qui troviamo la sua dichiarazione:

PRIVATE SUB prepareev(type AS Byte, flags AS Byte, ts AS Integer) AS Pointer
  DIM p AS Pointer
  DIM i AS Integer

I parametri della funzione riflettono ciò a cui siamo interessati - per esempio, noi non siamo interessati al campo "tag" e perciò non passiamo alcun valore per esso. Il primo passo è ripulire l'evento per essere sicuri di non trovare dati indesiderati:

'ripulisce l'evento 
p = ev 
FOR i = 1 TO SIZE_OF_SEQEV
  WRITE #p, 0, 1
  INC p
NEXT

Il puntatore "p" punta all'inizio dell'evento con "p = ev". Con un ciclo FOR-NEXT, viene scritto uno stream di zeri. L'istruzione "WRITE #p, 0, 1" scrive nell'area puntata da #p il valore 0 usando un byte. Si specifica "1" (per esempio 1 byte) perché il secondo parametro 0 è una costante intera e Gambas pensa debba essere scritta in 4 bytes (o 8) - lo forziamo ad usarne uno. Se invece di una costante avessimo usato una variabile di tipo byte Gambas ne avrebbe conosciuto la dimensione e avremo potuto evitare di specificarla. Ma attenzione! Questo lo si ottiene solo con le variabili, con le costanti no (forse un bug di Gambas 2, suppongo). Un migliore algoritmo di pulizia potrebbe scrivere 4 byte in una volta e ridurre il tempo di esecuzione di 1/4. Un'altra buona strada sarebbe quella di riscrivere solo i campi a cui siamo interessati e ripulirli solo se sappiamo che essi sono sporchi. Dopo la pulizia dell'evento passiamo al riempimento dei campi utili. Ancora, puntiamo il puntatore "p" alla posizione corretta (l'abbiamo mosso, ricordi?), scrivere un valore e spostare ancora il puntatore.

p = ev 
WRITE #p, type 
p += 1 ' ora p punta al campo flag

Da notare che ora "WRITE #p" ha una sintassi leggermente diversa. Questa volta ad essere scritta è una variabile e perciò non c'è bisogno di dire a Gambas quanti bytes scrivere. Vogliamo scrivere un singolo byte e type e grande proprio un byte. Il resto della routine è la ripetizione di quanto abbiamo già visto. Nel punto in cui si scrive su timestamp, che in C è un unione, troviamo il seguente codice:

WRITE #p, ts ' timestamp p += 4

Questa singola istruzione scrive il primo dei due interi di "snd_seq_timestamp_t time". Poi

ts = 0 
WRITE #p, ts '2^ part (realtime event) 
p += 4

Bene, queste tre righe non sono necessarie. Ripuliamo tutta la memoria prima così non c'è bisogno di settare nessun campo a 0. Però dovremmo in ogni caso muovere il puntatore. I due precedenti blocchi di codice dovrebbero seguire

WRITE #p, ts ' timestamp 
p += 8

La subroutine prepareev() è chiamata da noteon() e noteoff() che continua a riempire l'evento con i suoi dati. La subroutine di noteon() è:

PUBLIC SUB noteon(ts AS Integer, channel AS Byte, note AS Byte, velocity AS Byte)
  DIM p AS Pointer
  DIM err AS Integer
  p = prepareev(SND_SEQ_EVENT_NOTEON, 0, ts) 
  WRITE #p, channel 
  INC p 
  WRITE #p, note 
  INC p 
  WRITE #p, velocity 
  err = snd_seq_event_output_buffer(handle, ev)

La subroutine accetta un timestamp "ts" che dice quando suonare la nota ed indica il momento in cui la coda è partita. Un timestamp pari a 0 o comunque un valore minore dell'attuale tempo della coda viene eseguito immediatamente. Se il timestamp è maggiore dell'attuale tempo della coda esso verrà eseguito in futo al momento opportuno. Ma ALSA può anche usare un timestamp relativo, con il setting di un flag nell'evento. La classe ALSA di Gambas usa questa proprietà inglobandola direttamente nel timestamp. Se il parametro "ts" di prepareev() è negativo la funzione inverte il segno e seta il relativo flag. Nel programma principale la routine btArpeggio_Click() produce tre note usando questa possibilità:


PUBLIC SUB btArpeggio_Click()
  alsa.noteon(0, 0, 60, 100)
  alsa.noteoff(-100, 0, 60, 100) 
  alsa.noteon(-100, 0, 64, 100) 
  alsa.noteoff(-200, 0, 64, 100)  
  alsa.noteon(-200, 0, 67, 100) 
  alsa.noteoff(-300, 0, 67, 100) 
  alsa.flush
END

La subroutine inizia con la prima nota all'istante 0 che significa "ora". Dopo 100 tick la nota viene fermata ed una nuova viene eseguita (timestam = -100 indica 100 ticks dopo lo 0) e così per le successive note. Dato che suonare le note è il lavoro più comune e visto che per ogni nota bisogna chiamare il noteon() ed il noteoff() la libreria in C di ALSA permette di eseguirlo in una sola volta. Questa è la routine:

' 32 note hanno la stessa durata 
' "staccato": ogni nota termina prima che la successiva incomincia 
PUBLIC SUB btStaccato_Click()
  DIM i AS Integer
  FOR i = 1 TO 32
    ' timestamp=10, 20, 30 relativo... con step di 10
    ' ma le note hanno durata = 5, non 10
     alsa.playnote(-10 * i, 0, 60 + i, 100, 5)
  NEXT 
  alsa.flush
END

Usando una singola chiamata a playnote() essa genererà al suo interno due eventi.

Ultima cosa, per spiegare, qui c'è un'approfondimento sull'algoritmo della drum machine. Lo scopo è quello di produrre un flusso di eventi ce sarà eseguito in maniera successiva. Dobbiamo preparare un gruppo di eventi in anticipo in modo che l'hardware ha dei dati su cui lavorare. Ma una drum machine può durare anche per un lungo periodo e noi non possiamo effettuare un buffer di tutti gli eventi - dobbiamo selezionarne un certo numero, ne troppi e ne pochi. Il "puntatore" interno della drum machine è sempre di una misura musicale più avanti di ciò che stiamo ascoltando. Noi non possiamo prevedere in maniera precisa quando i nuovi dati saranno necessari poiché il sequencer solleva gli eventi basandosi su una temporizzazione che può essere diversa dalla nostra. Senza un feedback dal sequencer è quasi impossibile rimanere in sincronia con esso. Il problema è ancora più complesso se volgiamo vedere il sequencer cosa sta eseguendo in un determinato momento. Ciò è risolvibile aggiungendo allo stream degli eventi che non devono creare dei suoni ma che devono ritornare quelli già in esecuzione: un eco. Il sequencer riceve questi eventi aggiuntivi e li fa tornare a noi nel momento esatto. Quando vediamo l'eco possiamo sapere in che punto è il sequencer. La drum machine scritta in Gambas restituisce un eco ogni quarto ed usa queste informazioni per dare un feedback visivo. Questi eco possono contenere alcuni dati degli utenti - così da distinguerli nel programma. Il problema di questo programma è che l'interfaccia ALSA non provvedono a richiamare il segnale quando l'evento è pronto per essere letto (anche se lo facesse Gambas 2 non potrebbe usarlo). Questo è simulato dalla libreria ALSA scritta in C che genera un evento in Gambas quando un evento di tipo eco viene letto ma la stessa classe C fa uso di un polling per interrogare ALSA. La frequenza di questo polling può variare mediante lo slide "poll freq" nel programma principale. Settando tale frequenza ad un basso valore si noterà un imprecisione nel feedback visuale della drum machine ma la precisione della musica non dovrebbe subire effetti.

Appendice

That's really thiknnig out of the box. Thanks!

Precisazioni

Si riporta di seguito la discussione (in inglese) tra Doriano Blengino e B. Minisini sull'uso d READ e WRITE con i puntatori nella versione di Gambas 3.

[B. Minisini] ...... It is explicitely said that "WRITE #Pointer" is not supported anymore in Gambas 3, in the WRITE documentation page. And so on for the READ instruction.

[Doriano] Sorry, I think that it is not so clear. "No more supported", or equivalent semantics, is totally absent from the page. It is true that there is a frame specifying the syntax for READ in Gambas3, but it speaks only about "READ #Stream". This makes me think that the "READ #Stream" syntax or behavior changed, not necessarily that pointers are no more valid. Perhaps would be better to say something more in the first frame, the one just below the title of the page: "WARNING! The syntax has changed in Gambas 3. READ/WRITE with pointers is no more supported in Gambas 3. See below." You said that you removed this syntax because of possible problems with memory alignment. What about BytePtr(), SinglePtr() and the alike? Do they not suffer from the same problem? (Just a curiosity).

[B. Minisini] The Gambas 3 structures were implemented for the use of extern C structures. Moreover, in Gambas 3, you can now use Gambas functions as C function pointer (as known as "callbacks") almost transparently.

[Doriano] I saw that alsa does not really provides a callback - there is something about in an undocumented source (an utility for alsa).