12.7. Callback dei canali#

L’oggetto backend passato a protocol.register() è una classe Python. La libreria del protocollo non chiede alla classe quali metodi implementa; ispeziona l’istanza e collega quelli che trova. Questa introspezione è ciò che rende flessibile l’interfaccia del backend: il backend utile più piccolo ha due metodi, quello più elaborato ne ha dodici, e l’applicazione aderisce a ciascuna funzionalità un metodo alla volta.

12.7.1. Le regole di introspezione#

Quando protocol.register() viene eseguito, la libreria scorre un elenco fisso di nomi richiamabili e collega ciascuno di quelli che trova nell’istanza del backend:

  • Aggiungere read alla classe attiva CHANNEL_FLAG_READ. Una chiamata dell’host a channel_read() raggiunge il backend solo se questo flag è impostato.

  • Aggiungere write attiva CHANNEL_FLAG_WRITE, abilitando channel_write().

  • Aggiungere lock e unlock attiva CHANNEL_FLAG_LOCK, consentendo all’host di bloccare il canale per una lettura atomica multi-pacchetto.

  • Aggiungere poll consente all’host di chiedere «c’è qualcosa di pronto?» a basso costo, senza forzare una lettura completa.

I metodi mancanti non sono errori: la libreria del protocollo lascia semplicemente disabilitata la funzionalità corrispondente. Un backend con solo size e read è perfettamente valido; è un canale dati di sola lettura.

12.7.2. Un canale sensore di sola lettura#

Un canale sensore che pubblica una lettura aggiornata ogni volta che l’host la richiede, rifiutando le scritture dell’host, utilizza quattro dei callback:

import protocol
import struct

class TempChannel:
    def __init__(self, read_sensor):
        self._read_sensor = read_sensor
        self._buf = b''
        self._fresh = False

    def poll(self):
        # Tell the host whether a reading is waiting.
        return self._fresh

    def size(self):
        # Sample fresh data on every host-side size query.
        value = self._read_sensor()
        self._buf = struct.pack('<f', value)
        self._fresh = True
        return len(self._buf)

    def read(self, offset, size):
        end = offset + size
        if end >= len(self._buf):
            self._fresh = False
        return self._buf[offset:end]

protocol.register(name='temp', backend=TempChannel(read_temperature))

Esaminiamo cosa fa ciascun metodo:

  • poll restituisce il flag di freschezza. L’host lo chiama prima di leggere e salta del tutto la lettura quando restituisce False. Questo evita il costo del round-trip per «nessun dato nuovo ancora».

  • size rigenera il buffer su richiesta e ne riporta la lunghezza. Eseguire qui il campionamento significa che il backend non ha bisogno di un task in background: ogni chiamata dell’host guida ogni misurazione.

  • read restituisce una porzione del buffer. La libreria del protocollo può chiamarlo più di una volta quando il buffer è più grande del payload massimo negoziato; l’argomento offset scorre i frammenti.

  • L’assenza di write significa che le scritture dell’host vengono rifiutate a livello di framing, prima che il backend sia coinvolto.

12.7.3. L’insieme completo dei callback#

Per riferimento, ogni metodo che la libreria cerca su un backend:

Metodo

Restituisce

Scopo

init(self)

object

Inizializzazione opzionale una tantum quando il canale si collega per la prima volta a un host. Restituisce qualsiasi valore non None in caso di successo.

poll(self)

bool

Restituisce True quando i dati sono disponibili.

lock(self)

bool

Acquisisce il canale per un trasferimento atomico multi-pacchetto.

unlock(self)

bool

Rilascia un lock precedente.

size(self)

int

Numero di byte attualmente leggibili dal canale.

shape(self)

tuple

Fino a quattro interi che descrivono la struttura dei dati (ad es. altezza, larghezza, numero di byte dell’immagine). Usato dall’host per spacchettare buffer tipizzati.

read(self, offset, size)

bytes

Restituisce fino a size byte a partire da offset. Chiamato una volta per frammento quando il payload supera il massimo negoziato.

readp(self, offset, size)

bytes

Variante zero-copy di read: la memoria del buffer deve rimanere valida per tutta la durata del trasferimento.

write(self, offset, data)

int

L’host ha scritto data a offset. data è una vista bytearray nel buffer di ricezione del livello del protocollo: copia ciò che vuoi conservare prima di restituire il controllo.

ioctl(self, cmd, length, arg)

int

Opcode definito dall’applicazione al di fuori del modello read/write. Un valore di ritorno negativo è un errore.

flush(self)

object

Scarta qualsiasi dato bufferizzato. Chiamato quando l’host vuole reimpostare il canale.

is_active(self)

bool

Significativo solo sui backend che rappresentano un trasporto fisico (i canali USB integrati). I canali applicativi non ne hanno bisogno.

Questa è l’intera interfaccia del backend. Dodici nomi di metodi, tutti opzionali, e la libreria del protocollo decide cosa può fare ciascun canale in base a quali sono presenti.