12.7. 通道回呼函式

傳遞給 protocol.register() 的後端物件是一個 Python 類別。協定函式庫不會詢問該類別實作了哪些方法,而是檢視實例並接上它所找到的方法。正是這種自我檢視(introspection)讓後端介面具有彈性:最精簡的可用後端只需兩個方法,最完整的則有十二個,而應用程式可以一次一個方法地選擇加入各項功能。

12.7.1. 自我檢視規則

protocol.register() 執行時,函式庫會走訪一份固定的可呼叫名稱清單,並將它在後端實例上找到的每一個都繫結起來:

  • 在類別中加入 read 會開啟 CHANNEL_FLAG_READ。只有在設定了此旗標時,主機對 channel_read() 的呼叫才能到達後端。

  • 加入 write 會開啟 CHANNEL_FLAG_WRITE,啟用 channel_write()

  • 加入 lockunlock 會開啟 CHANNEL_FLAG_LOCK,讓主機能夠鎖定通道以進行多封包的不可分割讀取。

  • 加入 poll 讓主機能夠廉價地詢問「有沒有任何資料就緒?」,而不必強制執行一次完整的讀取。

缺少方法並不是錯誤——協定函式庫只會讓對應的功能保持停用。一個只有 sizeread 的後端是完全有效的;它就是一個唯讀的資料通道。

12.7.2. 唯讀的感測器通道

一個每次主機詢問時都會發布最新讀數、並拒絕主機寫入的感測器通道,運用了其中四個回呼函式::

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))

逐一說明每個方法的作用:

  • poll 回傳新鮮度旗標。主機在讀取前會呼叫它,當它回傳 False 時便完全略過讀取。這在「尚無新資料」的情況下省下了往返的成本。

  • size 會依需求重新產生緩衝區並回報其長度。在此處進行取樣意味著後端不需要背景工作——每次測量都由主機呼叫驅動。

  • read 回傳緩衝區的一個切片。當緩衝區大於協商出的最大酬載(payload)時,協定函式庫可能會多次呼叫它;offset 引數會逐一走訪這些片段。

  • 沒有 write 表示主機寫入會在框架(framing)層就被拒絕,根本不會涉及後端。

12.7.3. 完整的回呼函式集

供參考,以下是函式庫會在後端上尋找的每一個方法:

方法

回傳值

用途

init(self)

object

當通道首次繫結到主機時,選擇性的一次性初始化。成功時回傳任何非 None 的值。

poll(self)

bool

當有資料可用時回傳 True

lock(self)

bool

取得通道以進行不可分割的多封包傳輸。

unlock(self)

bool

釋放先前的 lock

size(self)

int

目前可從通道讀取的位元組數。

shape(self)

tuple

最多四個整數,用以描述資料結構(例如影像高度、寬度、位元組計數)。供主機用來解包具型別的緩衝區。

read(self, offset, size)

bytes

offset 開始回傳最多 size 個位元組。當酬載超過協商出的最大值時,每個片段呼叫一次。

readp(self, offset, size)

bytes

read 的零複製(zero-copy)變體:緩衝區的記憶體必須在整個傳輸期間保持有效。

write(self, offset, data)

int

主機在 offset 處寫入了 datadata 是指向協定層接收緩衝區的 bytearray 檢視——在回傳前請先複製出你想保留的內容。

ioctl(self, cmd, length, arg)

int

讀取/寫入模型之外、由應用程式定義的運算碼(opcode)。回傳負值表示錯誤。

flush(self)

object

丟棄任何已緩衝的資料。當主機想要重設通道時呼叫。

is_active(self)

bool

只在代表實體傳輸的後端上(內建的 USB 通道)才有意義。應用程式通道不需要它。

這就是整個後端介面。十二個方法名稱,全部都是選擇性的,協定函式庫會根據存在哪些方法來決定每個通道能做什麼。