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.