Banjo 教學課程

Banjo 是一種「譯者」(例如 FIDL 的 fidlc),這種程式可將介面定義語言 (IDL) 轉換為目標語言特定檔案。

本教學課程的結構如下:

  • Banjo 簡介
  • 簡易範例 (I2C)
  • 範例中產生的程式碼

還有一個參考資料章節:

  • 內建關鍵字和原始類型的清單

總覽

Banjo 會產生 C 和 C++ 程式碼,可供通訊協定實作器和通訊協定使用者使用。

簡單範例

首先,讓我們來看看比較簡單的 Banjo 規格。 以下是 //sdk/banjo/fuchsia.hardware.i2cimpl/i2cimpl.fidl 檔案:

請注意,本教學課程的程式碼範例中的行數並非檔案的一部分,

[01] // Copyright 2018 The Fuchsia Authors. All rights reserved.
[02] // Use of this source code is governed by a BSD-style license that can be
[03] // found in the LICENSE file.
[04] @available(added=7)
[05] library fuchsia.hardware.i2cimpl;
[06]
[07] using zx;
[08]
[09] const I2C_IMPL_10_BIT_ADDR_MASK uint32 = 0xF000;
[10] /// The maximum number of I2cImplOp's that may be passed to Transact.
[11] const I2C_IMPL_MAX_RW_OPS uint32 = 8;
[12] /// The maximum length of all read or all write transfers in bytes.
[13] const I2C_IMPL_MAX_TOTAL_TRANSFER uint32 = 4096;
[14]
[15] /// See `Transact` below for usage.
[16] type I2cImplOp = struct {
[17]     address uint16;
[18]     @buffer
[19]     @mutable
[20]     data vector<uint8>:MAX;
[21]     is_read bool;
[22]     stop bool;
[23] };
[24]
[25] /// Low-level protocol for i2c drivers.
[26] @transport("Banjo")
[27] @banjo_layout("ddk-protocol")
[28] protocol I2cImpl {
[29]     /// First bus ID that this I2cImpl controls, zero-indexed.
[30]     GetBusBase() -> (struct {
[31]         base uint32;
[32]     });
[33]     /// Number of buses that this I2cImpl supports.
[34]     GetBusCount() -> (struct {
[35]         count uint32;
[36]     });
[37]     GetMaxTransferSize(struct {
[38]         bus_id uint32;
[39]     }) -> (struct {
[40]         s zx.Status;
[41]         size uint64;
[42]     });
[43]     /// Sets the bitrate for the i2c bus in KHz units.
[44]     SetBitrate(struct {
[45]         bus_id uint32;
[46]         bitrate uint32;
[47]     }) -> (struct {
[48]         s zx.Status;
[49]     });
[50]     /// |Transact| assumes that all ops buf are not null.
[51]     /// |Transact| assumes that all ops length are not zero.
[52]     /// |Transact| assumes that at least the last op has stop set to true.
[53]     Transact(struct {
[54]         bus_id uint32;
[55]         op vector<I2cImplOp>:MAX;
[56]     }) -> (struct {
[57]         status zx.Status;
[58]     });
[59] };

定義了可讓應用程式讀取及寫入 I2C 匯流排資料的介面。在 I2C 匯流排中,資料必須先寫入裝置,才能徵求回應。如果需要回應,可以從裝置讀取該回應。(舉例來說,設定僅限寫入的註冊時,不需要回應。)

讓我們逐行查看個別元件:

  • [05]library 指令會指示 Banjo 編譯器在產生的輸出內容上應使用哪個前置字串,將其視為命名空間指定碼。
  • [07]using 指令會指示 Banjo 納入 zx 程式庫。
  • [09] [11][13] — 這推出了兩個常數,供程式設計師使用。
  • [16 .. 23] — 這些結構會定義名為 I2cImplOp 的結構,程式設計師隨後會用於將資料轉移至匯流排的資料。
  • [26 .. 59] — 這些行定義了此 Banjo 規格提供的介面方法;我們會在下方詳細說明。

請別混淆 [50 .. 52] (和其他位置) 中的註解,這些註解指的是預先發送至產生的來源的註解。所有以「///」開頭 (三條斜線) 的留言都是「流動」的註解。 一般註解 (即「//」) 適用於目前的模組。如此一來,當您查看產生的程式碼時,就會知道問題所在。

作業結構

在 I2C 範例中,struct I2cImplOp 結構定義了四個元素:

元素 類型 使用
address uint16 與公車互動的方塊地址
data vector<voidptr> 包含送往及選擇從公車接收的資料
is_read bool 的旗標,表示所需的讀取功能
stop bool 表示作業完成後應傳送停止位元組的旗標

這個結構會定義通訊協定實作 (驅動程式庫) 和通訊協定使用者 (使用公車的程式) 之間的通訊區域。

介面

更有趣的部分是 protocol 規格。

目前我們會略過 @transport("Banjo") (第 [26] 行) 和 @banjo_layout("ddk-protocol") (第 [27] 行) 屬性,但會在下方的「屬性」中改回這些屬性。

protocol 區段定義了五個介面方法:

  • GetBusBase
  • GetBusCount
  • GetMaxTransferSize
  • SetBitrate
  • Transact

如果不深入探討內部作業 (這並非有關 I2C 的教學課程),我們就來看看這些內容如何翻譯成目標語言。我們會另外查看 C 和 C++ 實作,並使用 C 說明納入 C++ 版本常見的結構定義。

目前支援產生 C 和 C++ 程式碼,日後預計支援 Rust。

C

C 實作方式相當簡單:

  • structunion 幾乎直接對應到其 C 語言對應的項目。
  • enum 和常數會以 #define 巨集的形式產生。
  • protocol 會產生為兩個 struct
    • 函式資料表
    • 一個結構體,其中包含指向函式資料表和背景資訊的指標。
  • 也會產生部分輔助函式。

C 版本會產生至 $BUILD_DIR/fidling/gen/sdk/banjo/fuchsia.hardware.i2cimpl/fuchsia.hardware.i2cimpl_banjo_c/fuchsia/hardware/i2cimpl/c/banjo.h

檔案相對過長,我們會分成數個部分進行。

樣板

第一部分含有樣板,不需進一步查看即可顯示:

[01] // Copyright 2018 The Fuchsia Authors. All rights reserved.
[02] // Use of this source code is governed by a BSD-style license that can be
[03] // found in the LICENSE file.
[04]
[05] // WARNING: THIS FILE IS MACHINE GENERATED. DO NOT EDIT.
[06] // Generated from the fuchsia.hardware.i2cimpl banjo file
[07]
[08] #pragma once
[09]
[10]
[11] #include <zircon/compiler.h>
[12] #include <zircon/types.h>
[13]
[14] __BEGIN_CDECLS

轉寄宣告

接下來是結構和函式的前向宣告:

[16] // Forward declarations
[17] typedef struct i2c_impl_op i2c_impl_op_t;
[18] typedef struct i2c_impl_protocol i2c_impl_protocol_t;
[19] typedef struct i2c_impl_protocol_ops i2c_impl_protocol_ops_t;
...
[26] // Declarations
[27] // See `Transact` below for usage.
[28] struct i2c_impl_op {
[29]     uint16_t address;
[30]     uint8_t* data_buffer;
[31]     size_t data_size;
[32]     bool is_read;
[33]     bool stop;
[34] };

請注意,[17 .. 19] 行只會宣告類型,實際上並未定義函式的結構或原型。

請注意「排行」註解 (例如原始 .fidl 檔案行 [15]) 如何排入產生的程式碼 (上方第 [27] 行),只要刪除一個斜線,這些註解看起來就會像一般註解。

[28 .. 34 行是通告,這是從上方 .fidl 檔案 (第 [16[16 .. 23) 中直接對應 struct I2cImplOp 的那行程式碼。

Astute C 程式設計師會立即看到 C++ 樣式 vector<voidptr> data (原始 .fidl 檔案行 [20]) 如何在 C 中處理:轉換成指標 (data_buffer) 和大小 (「data_size」)。

就命名而言,基礎名稱為 data (如 .fidl 檔案所示)。針對 voidptr 的向量,轉譯器會附加 _buffer_size,將 vector 轉換為 C 相容的結構。針對所有其他向量類型,轉譯器會改為附加 _list_count (以便程式碼可讀)。

常數

接下來,我們可以看到 const uint32 常數轉換為 #define 陳述式:

[20] // The maximum length of all read or all write transfers in bytes.
[21] #define I2C_IMPL_MAX_TOTAL_TRANSFER UINT32_C(4096)
[22] // The maximum number of I2cImplOp's that may be passed to Transact.
[23] #define I2C_IMPL_MAX_RW_OPS UINT32_C(8)
[24] #define I2C_IMPL_10_BIT_ADDR_MASK UINT32_C(0xF000)

在 C 版本中,我們選擇 #define 而非「傳遞」,而是 const uint32_t 表示法,原因如下:

  • #define 陳述式只會在編譯時間存在,且會在每個使用網站上內嵌,而 const uint32_t 則會嵌入在二進位檔中。
  • #define 可進行更多編譯時間最佳化,例如使用常數值進行數學計算。

缺點就是我們不會取得類型安全,因此會顯示輔助程式巨集 (例如上述的 UINT32_C());這些巨集只會將常數轉換成適當的類型。

通訊協定結構

接著我們來切入重點

[36] struct i2c_impl_protocol_ops {
[37]     uint32_t (*get_bus_base)(void* ctx);
[38]     uint32_t (*get_bus_count)(void* ctx);
[39]     zx_status_t (*get_max_transfer_size)(void* ctx, uint32_t bus_id, uint64_t* out_size);
[40]     zx_status_t (*set_bitrate)(void* ctx, uint32_t bus_id, uint32_t bitrate);
[41]     zx_status_t (*transact)(void* ctx, uint32_t bus_id, const i2c_impl_op_t* op_list, size_t op_count);
[42] };

這項操作會建立結構定義,當中包含原始 .fidl 檔案中第 [30][34][37][44][43] 行定義的五個 protocol 方法。

請注意,已進行名稱操縱,這就是將 protocol 方法名稱對應至 C 函式指標名稱的方式,以便您瞭解其呼叫的內容:

Banjo C 規則
Transact transact 將前置大寫轉換成小寫
GetBusBase get_bus_base 如上所述,然後將駝峰式大小寫轉換為底線分隔樣式
GetBusCount get_bus_count 同上
SetBitrate set_bitrate 同上
GetMaxTransferSize get_max_transfer_size 同上

接下來,介面定義會納入情境感知結構:

[45] struct i2c_impl_protocol {
[46]     i2c_impl_protocol_ops_t* ops;
[47]     void* ctx;
[48] };

最後,我們可以看到這五種方法的實際產生的程式碼:

[53] static inline uint32_t i2c_impl_get_bus_base(const i2c_impl_protocol_t* proto) {
[54]     return proto->ops->get_bus_base(proto->ctx);
[55] }
[56]
[57] // Number of buses that this I2cImpl supports.
[58] static inline uint32_t i2c_impl_get_bus_count(const i2c_impl_protocol_t* proto) {
[59]     return proto->ops->get_bus_count(proto->ctx);
[60] }
[61]
[62] static inline zx_status_t i2c_impl_get_max_transfer_size(const i2c_impl_protocol_t* proto, uint32_t bus_id, uint64_t* out_size) {
[63]     return proto->ops->get_max_transfer_size(proto->ctx, bus_id, out_size);
[64] }
[65]
[66] // Sets the bitrate for the i2c bus in KHz units.
[67] static inline zx_status_t i2c_impl_set_bitrate(const i2c_impl_protocol_t* proto, uint32_t bus_id, uint32_t bitrate) {
[68]     return proto->ops->set_bitrate(proto->ctx, bus_id, bitrate);
[69] }
[70]
[71] // |Transact| assumes that all ops buf are not null.
[72] // |Transact| assumes that all ops length are not zero.
[73] // |Transact| assumes that at least the last op has stop set to true.
[74] static inline zx_status_t i2c_impl_transact(const i2c_impl_protocol_t* proto, uint32_t bus_id, const i2c_impl_op_t* op_list, size_t op_count) {
[75]     return proto->ops->transact(proto->ctx, bus_id, op_list, op_count);
[76] }

前置字串和路徑

請注意,在方法名稱中加入前置字串 i2c_impl_ (從介面名稱,.fidl 檔案行 [28]) 如何加入方法名稱,因此 Transact 會變成 i2c_impl_transact 等等。以上是 .fidl 名稱與其 C 對等項目之間的對應關係。

此外,library 名稱 (.fidl 檔案中的第 [05] 行) 會轉換為 include 路徑:因此 library fuchsia.hardware.i2cimpl 表示 <fuchsia/hardware/i2cimpl/c/banjo.h> 的路徑。

C++

C++ 程式碼比 C 版本稍微複雜。一起來看看吧。

Banjo 轉譯器會產生三個檔案:第一個是上述的 C 檔案,另外兩個檔案則位於 $BUILD_DIR/fidling/gen/sdk/banjo/fuchsia.hardware.i2cimpl/fuchsia.hardware.i2cimpl_banjo_c/fuchsia/hardware/i2cimpl/cpp/ 底下

  • i2cimpl.h:程式應包含的檔案,以及
  • i2cimpl-internal.hi2cimpl.h 包含的內部檔案

「internal」檔案包含宣告和宣告,我們可以放心略過。

i2cimpl.h 的 C++ 版本相當長,我們會以較小的片段加以說明。以下是我們將探討的內容總覽「地圖」,當中顯示每個片段的起始行數:

Line 章節
1 樣板
200 自動產生的用量註解
61 I2cImplProtocol 類別
112 I2cImplProtocolClient 類別

樣板

樣板是你預期的結果:

[001] // Copyright 2018 The Fuchsia Authors. All rights reserved.
[002] // Use of this source code is governed by a BSD-style license that can be
[003] // found in the LICENSE file.
[004]
[005] // WARNING: THIS FILE IS MACHINE GENERATED. DO NOT EDIT.
[006] // Generated from the fuchsia.hardware.i2cimpl banjo file
[007]
[008] #pragma once
[009]
[010] #include <ddktl/device-internal.h>
[011] #include <fuchsia/hardware/i2cimpl/c/banjo.h>
[012] #include <lib/ddk/device.h>
[013] #include <lib/ddk/driver.h>
[014] #include <zircon/assert.h>
[015] #include <zircon/compiler.h>
[016] #include <zircon/types.h>
[017]
[018] #include "banjo-internal.h"

#include 具有多個 DDK 和 OS 標頭,包括:

  • 標頭的 C 版本 (第 [011] 行,也就是說,上方 C 章節中的所有討論內容也適用)。
  • 產生的 i2cimpl-internal.h 檔案 (第 [018] 行)。

接著是「自動產生的使用註解」部分;我們之後會再來到之後,因為看到實際的類別宣告後,會變得更加合理。

兩個類別宣告都包含在 DDK 命名空間中:

[057] namespace ddk {
...
[214] } // namespace ddk

I2cImplProtocolClient 包裝函式類別

I2cImplProtocolClient 類別是 i2c_impl_protocol_t 結構周圍的簡易包裝函式 (在 C include 檔案、第 [45] 行定義,請參閱上述的通訊協定結構一節)。

[112] class I2cImplProtocolClient {
[113] public:
[114]     I2cImplProtocolClient()
[115]         : ops_(nullptr), ctx_(nullptr) {}
[116]     I2cImplProtocolClient(const i2c_impl_protocol_t* proto)
[117]         : ops_(proto->ops), ctx_(proto->ctx) {}
[118]
[119]     I2cImplProtocolClient(zx_device_t* parent) {
[120]         i2c_impl_protocol_t proto;
[121]         if (device_get_protocol(parent, ZX_PROTOCOL_I2C_IMPL, &proto) == ZX_OK) {
[122]             ops_ = proto.ops;
[123]             ctx_ = proto.ctx;
[124]         } else {
[125]             ops_ = nullptr;
[126]             ctx_ = nullptr;
[127]         }
[128]     }
[129]
[130]     I2cImplProtocolClient(zx_device_t* parent, const char* fragment_name) {
[131]         i2c_impl_protocol_t proto;
[132]         if (device_get_fragment_protocol(parent, fragment_name, ZX_PROTOCOL_I2C_IMPL, &proto) == ZX_OK) {
[133]             ops_ = proto.ops;
[134]             ctx_ = proto.ctx;
[135]         } else {
[136]             ops_ = nullptr;
[137]             ctx_ = nullptr;
[138]         }
[139]     }
[140]
[141]     // Create a I2cImplProtocolClient from the given parent device + "fragment".
[142]     //
[143]     // If ZX_OK is returned, the created object will be initialized in |result|.
[144]     static zx_status_t CreateFromDevice(zx_device_t* parent,
[145]                                         I2cImplProtocolClient* result) {
[146]         i2c_impl_protocol_t proto;
[147]         zx_status_t status = device_get_protocol(
[148]                 parent, ZX_PROTOCOL_I2C_IMPL, &proto);
[149]         if (status != ZX_OK) {
[150]             return status;
[151]         }
[152]         *result = I2cImplProtocolClient(&proto);
[153]         return ZX_OK;
[154]     }
[155]
[156]     // Create a I2cImplProtocolClient from the given parent device.
[157]     //
[158]     // If ZX_OK is returned, the created object will be initialized in |result|.
[159]     static zx_status_t CreateFromDevice(zx_device_t* parent, const char* fragment_name,
[160]                                         I2cImplProtocolClient* result) {
[161]         i2c_impl_protocol_t proto;
[162]         zx_status_t status = device_get_fragment_protocol(parent, fragment_name,
[163]                                  ZX_PROTOCOL_I2C_IMPL, &proto);
[164]         if (status != ZX_OK) {
[165]             return status;
[166]         }
[167]         *result = I2cImplProtocolClient(&proto);
[168]         return ZX_OK;
[169]     }
[170]
[171]     void GetProto(i2c_impl_protocol_t* proto) const {
[172]         proto->ctx = ctx_;
[173]         proto->ops = ops_;
[174]     }
[175]     bool is_valid() const {
[176]         return ops_ != nullptr;
[177]     }
[178]     void clear() {
[179]         ctx_ = nullptr;
[180]         ops_ = nullptr;
[181]     }
[182]
[183]     // First bus ID that this I2cImpl controls, zero-indexed.
[184]     uint32_t GetBusBase() const {
[185]         return ops_->get_bus_base(ctx_);
[186]     }
[187]
[188]     // Number of buses that this I2cImpl supports.
[189]     uint32_t GetBusCount() const {
[190]         return ops_->get_bus_count(ctx_);
[191]     }
[192]
[193]     zx_status_t GetMaxTransferSize(uint32_t bus_id, uint64_t* out_size) const {
[194]         return ops_->get_max_transfer_size(ctx_, bus_id, out_size);
[195]     }
[196]
[197]     // Sets the bitrate for the i2c bus in KHz units.
[198]     zx_status_t SetBitrate(uint32_t bus_id, uint32_t bitrate) const {
[199]         return ops_->set_bitrate(ctx_, bus_id, bitrate);
[200]     }
[201]
[202]     // |Transact| assumes that all ops buf are not null.
[203]     // |Transact| assumes that all ops length are not zero.
[204]     // |Transact| assumes that at least the last op has stop set to true.
[205]     zx_status_t Transact(uint32_t bus_id, const i2c_impl_op_t* op_list, size_t op_count) const {
[206]         return ops_->transact(ctx_, bus_id, op_list, op_count);
[207]     }
[208]
[209] private:
[210]     i2c_impl_protocol_ops_t* ops_;
[211]     void* ctx_;
[212] };

建構函式分為四種:

  • ops_ctx_ 設為 nullptr 的預設動作 ([114]),
  • 初始化器 ([116]) 可擷取指標至 i2c_impl_protocol_t 結構,並根據其在結構內的名稱填入 ops_ctx_ 欄位;以及
  • zx_device_t 擷取 ops_ctx_ 資訊的初始化器 ([119])。
  • 初始化器 ([130]) (如上述),但從裝置片段取得 ops_ctx_

建議使用最後兩個建構函式,做法是:

ddk::I2cImplProtocolClient i2cimpl(parent);
if (!i2cimpl.is_valid()) {
  return ZX_ERR_*; // return an appropriate error
}
ddk::I2cImplProtocolClient i2cimpl(parent, "i2c-impl-fragment");
if (!i2cimpl.is_valid()) {
  return ZX_ERR_*; // return an appropriate error
}

以下是三種便利成員函式:

  • [171] GetProto() 會擷取 ctx_ops_ 成員至通訊協定結構,
  • [175] is_valid() 會傳回 bool,表示類別是否已透過通訊協定初始化;
  • [178] clear() 會使 ctx_ops_ 指標失效。

接下來,我們會找出 .fidl 檔案中指定的四個成員函式:

  • [138] GetBusBase()
  • [138] GetBusCount()
  • [138] GetMaxTransferSize() 以及
  • [138] SetBitrate()
  • [134] Transact()

運作方式與 include 檔案 C 版本的四個包裝函式函式類似,也就是透過個別的函式指標,將引數傳遞至呼叫。

事實上,請比較 C 版本的 i2c_impl_get_max_transfer_size()

[138] zx_status_t GetMaxTransferSize(size_t* out_size) const {
[139]     return ops_->get_max_transfer_size(ctx_, out_size);
[140] }

搭配使用 C++ 版本:

[138] zx_status_t GetMaxTransferSize(size_t* out_size) const {
[139]     return ops_->get_max_transfer_size(ctx_, out_size);
[140] }

如通告,此類別只會儲存作業和結構定義指標供日後使用,讓透過包裝函式呼叫變得更加簡單。

您也會發現,C++ 包裝函式函式沒有任何名稱操控作業,如要使用話,GetMaxTransferSize()GetMaxTransferSize()

I2cImplProtocol 混合類別

剛才就是這麼簡單。 在後續部分中,我們將探討 mixinsCRTP,或「好奇週期性範本模式」

我們先來瞭解類別的「形狀」(此為大綱用途刪除的註解行):

[060] template <typename D, typename Base = internal::base_mixin>
[061] class I2cImplProtocol : public Base {
[062] public:
[063]     I2cImplProtocol() {
[064]         internal::CheckI2cImplProtocolSubclass<D>();
[065]         i2c_impl_protocol_ops_.get_bus_base = I2cImplGetBusBase;
[066]         i2c_impl_protocol_ops_.get_bus_count = I2cImplGetBusCount;
[067]         i2c_impl_protocol_ops_.get_max_transfer_size = I2cImplGetMaxTransferSize;
[068]         i2c_impl_protocol_ops_.set_bitrate = I2cImplSetBitrate;
[069]         i2c_impl_protocol_ops_.transact = I2cImplTransact;
[070]
[071]         if constexpr (internal::is_base_proto<Base>::value) {
[072]             auto dev = static_cast<D*>(this);
[073]             // Can only inherit from one base_protocol implementation.
[074]             ZX_ASSERT(dev->ddk_proto_id_ == 0);
[075]             dev->ddk_proto_id_ = ZX_PROTOCOL_I2C_IMPL;
[076]             dev->ddk_proto_ops_ = &i2c_impl_protocol_ops_;
[077]         }
[078]     }
[079]
[080] protected:
[081]     i2c_impl_protocol_ops_t i2c_impl_protocol_ops_ = {};
[082]
[083] private:
...
[085]     static uint32_t I2cImplGetBusBase(void* ctx) {
[086]         auto ret = static_cast<D*>(ctx)->I2cImplGetBusBase();
[087]         return ret;
[088]     }
...
[090]     static uint32_t I2cImplGetBusCount(void* ctx) {
[091]         auto ret = static_cast<D*>(ctx)->I2cImplGetBusCount();
[092]         return ret;
[093]     }
[094]     static zx_status_t I2cImplGetMaxTransferSize(void* ctx, uint32_t bus_id, uint64_t* out_size) {
[095]         auto ret = static_cast<D*>(ctx)->I2cImplGetMaxTransferSize(bus_id, out_size);
[096]         return ret;
[097]     }
...
[099]     static zx_status_t I2cImplSetBitrate(void* ctx, uint32_t bus_id, uint32_t bitrate) {
[100]         auto ret = static_cast<D*>(ctx)->I2cImplSetBitrate(bus_id, bitrate);
[101]         return ret;
[102]     }
...
[106]     static zx_status_t I2cImplTransact(void* ctx, uint32_t bus_id, const i2c_impl_op_t* op_list, size_t op_count) {
[107]         auto ret = static_cast<D*>(ctx)->I2cImplTransact(bus_id, op_list, op_count);
[108]         return ret;
[109]     }
[110] };

I2CImplProtocol 類別繼承自第二個範本參數指定的基礎類別。如未指定,預設值為 internal::base_mixin,且不會出現特殊魔術。不過,如果已明確指定基礎類別,則基礎類別應為 ddk::base_protocol (在這種情況下,系統會新增其他斷言),以便再次確認僅有一個混合是基本通訊協定。此外,系統會將特殊的 DDKTL 欄位設為在驅動程式庫觸發 DdkAdd() 時,自動將這個通訊協定註冊為基礎通訊協定。

建構函式會呼叫內部驗證函式 CheckI2cImplProtocolSubclass() [32] (產生的 i2c-impl-internal.h 檔案所定義),其中包含多個 static_assert() 呼叫。 D 類別應實作五個成員函式 (I2cImplGetBusBase()I2cIImplGetBusCount()I2cImplGetMaxTransferSize()I2cImplSetBitrate()I2cImplTransact()),以便讓靜態方法正常運作。如果 D 未提供這些屬性,編譯器會在 (沒有靜態斷言) 時產生輕微範本錯誤。靜態斷言可以產生人類可以理解的診斷錯誤。

接下來,五個指標對函式作業成員 (get_bus_baseget_bus_countget_max_transfer_sizeset_bitratetransact) 有繫結 (行 [065 .. 069])。

最後,constexpr 運算式會在必要時提供預設初始化。

使用 Mixin 類別

您可以按照下列方式使用 I2cImplProtocol 類別 (來自 //src/devices/i2c/drivers/intel-i2c/intel-i2c-controller.h):

[135] class IntelI2cController : public IntelI2cControllerType,
[136]                            public ddk::I2cImplProtocol<IntelI2cController, ddk::base_protocol> {
[137]  public:
[138]   explicit IntelI2cController(zx_device_t* parent)
[139]       : IntelI2cControllerType(parent), pci_(parent, "pci") {}
[140]
[141]   static zx_status_t Create(void* ctx, zx_device_t* parent);
[142]
[143]   void DdkInit(ddk::InitTxn txn);
...
[170]   uint32_t I2cImplGetBusBase();
[171]   uint32_t I2cImplGetBusCount();
[172]   zx_status_t I2cImplGetMaxTransferSize(const uint32_t bus_id, size_t* out_size);
[173]   zx_status_t I2cImplSetBitrate(const uint32_t bus_id, const uint32_t bitrate);
[174]   zx_status_t I2cImplTransact(const uint32_t bus_id, const i2c_impl_op_t* op_list,
[175]                               const size_t op_count);
[176]
[177]   void DdkUnbind(ddk::UnbindTxn txn);
[178]   void DdkRelease();
[179]
[180]  private:
...

這裡可以看到 class IntelI2cController 繼承自 DDK 的 I2cImplProtocol,並以引數形式提供給範本,這就是「mixin」概念。這會導致類別範本定義中的 IntelI2cController 替代 D (來自上述的 i2c-impl.h 標頭檔案、行 [086][091][95][100][107])。

光看 I2cImplGetMaxTransferSize() 函式做為範例,其實際上可類似於原始碼讀取:

[094] static zx_status_t I2cImplGetMaxTransferSize(void* ctx, uint32_t bus_id, uint64_t* out_size) {
[095]     auto ret = static_cast<IntelI2cController*>(ctx)->I2cImplGetMaxTransferSize(bus_id, out_size);
[096]     return ret;
[097] }

最終會刪除程式碼中的「投放至自行管理」樣板。 由於類型資訊會在 DDK 界線處清除,因此這是必要轉換。請注意,結構定義 ctxvoid * 指標。

自動產生的留言

Banjo 會在 include 檔案中自動產生註解,其中包括上述問題的摘要:

[020] // DDK i2cimpl-protocol support
[021] //
[022] // :: Proxies ::
[023] //
[024] // ddk::I2cImplProtocolClient is a simple wrapper around
[025] // i2c_impl_protocol_t. It does not own the pointers passed to it.
[026] //
[027] // :: Mixins ::
[028] //
[029] // ddk::I2cImplProtocol is a mixin class that simplifies writing DDK drivers
[030] // that implement the i2c-impl protocol. It doesn't set the base protocol.
[031] //
[032] // :: Examples ::
[033] //
[034] // // A driver that implements a ZX_PROTOCOL_I2C_IMPL device.
[035] // class I2cImplDevice;
[036] // using I2cImplDeviceType = ddk::Device<I2cImplDevice, /* ddk mixins */>;
[037] //
[038] // class I2cImplDevice : public I2cImplDeviceType,
[039] //                      public ddk::I2cImplProtocol<I2cImplDevice> {
[040] //   public:
[041] //     I2cImplDevice(zx_device_t* parent)
[042] //         : I2cImplDeviceType(parent) {}
[043] //
[044] //     uint32_t I2cImplGetBusBase();
[045] //
[046] //     uint32_t I2cImplGetBusCount();
[047] //
[048] //     zx_status_t I2cImplGetMaxTransferSize(uint32_t bus_id, uint64_t* out_size);
[049] //
[050] //     zx_status_t I2cImplSetBitrate(uint32_t bus_id, uint32_t bitrate);
[051] //
[052] //     zx_status_t I2cImplTransact(uint32_t bus_id, const i2c_impl_op_t* op_list, size_t op_count);
[053] //
[054] //     ...
[055] // };

使用 Banjo

目前我們已看到 I2C 驅動程式庫產生的程式碼,接著來看看如何使用。

需完成 @@@

參考資料

@@@ 我們應該在此列出所有內建關鍵字和基本類型

屬性

記得在上述範例中,protocol 區段有兩個屬性:@transport("Banjo")@banjo_layout("ddk-protocol") 屬性。

傳輸屬性

所有 Banjo 通訊協定都必須設定 @transport("Banjo"),以表示使用的是 Banjo,而非 FIDL。

banjo_layout 屬性

protocol 前方的行是 banjo_layout 屬性:

[27] @banjo_layout("ddk-protocol")
[28] protocol I2cImpl {

這個屬性適用於下一個項目;在這個例子中,就是整個 protocol。每個介面只能使用一個版面配置。

目前支援的 BanjoLayout 屬性類型有 3 種:

  • ddk-protocol
  • ddk-interface
  • ddk-callback

為了瞭解這些版面配置類型的運作方式,我們假設我們有兩個驅動程式:AB。驅動程式 A 會產生裝置,接著 B 連接至裝置 (讓 B 成為 A 的子項)。

如果 B 透過 device_get_protocol() 查詢 DDK 以做為其父項的「通訊協定」,就會取得 ddk-protocolddk-protocol 是父項提供給子項的一組回呼。

其中一個通訊協定函式可註冊「反向通訊協定」,接著子項會提供一組回呼,供父項改為觸發。這是 ddk-interface

從程式碼產生的角度來看,這兩個 (ddk-protocolddk-interface) 幾乎完全相同,但有些微的命名差異 (ddk-protocol 會自動將「通訊協定」一詞附加到已產生的結構體 / 類別結尾,但 ddk-interface 不會)。

ddk-callbackddk-interface 的輕微最佳化項目,適用於介面只有單一函式。與其產生以下兩種結構:

struct interface {
   void* ctx;
   interface_function_ptr_table* callbacks;
};

struct interface_function_ptr_table {
   void (*one_function)(...);
}

ddk-callback 會產生單一結構,並將函式指標內嵌:

struct callback {
  void* ctx;
  void (*one_function)(...);
};

非同步屬性

如需 @async 屬性的範例,請參閱 fuchsia.hardware.block Block 通訊協定。

protocol 區段中,可以看到 @async 屬性:

[254] protocol Block {
...       /// comments (removed)
[268]     @async

@async 屬性能讓通訊協定訊息不同步。系統會自動產生回呼類型,其中輸出引數是回呼的輸入內容。原始方法的簽名中不會包含任何指定的輸出參數。

在上述通訊協定中,Queue 方法宣告為:

[268] @async
[269] Queue(resource struct {
[270]     @in_out
[271]     txn BlockOp;
[272] }) -> (resource struct {
[273]     status zx.Status;
[274]     @mutable
[275]     op BlockOp;
[276] });

將上述項目與 @async 屬性搭配使用時,這表示我們希望 Banjo 叫用回呼函式,藉此處理輸出資料 (上述第二個 BlockOp,代表區塊裝置的資料)。

以下聊聊如何使用。 我們會透過第一個 BlockOp 引數將資料傳送至區塊裝置。一段時間過後,封鎖裝置可能會根據我們的要求產生資料。 因為我們指定了 @async,所以 Banjo 會產生函式,以將回呼函式視為輸入內容。

在 C 中,以下兩行 (來自 block.h 檔案) 很重要:

[085] typedef void (*block_queue_callback)(void* ctx, zx_status_t status, block_op_t* op);
...
[211] void (*queue)(void* ctx, block_op_t* txn, block_queue_callback callback, void* cookie);

在 C++ 中,我們在兩個地方參照回呼:

[113] static void BlockQueue(void* ctx, block_op_t* txn, block_queue_callback callback, void* cookie) {
[114]     static_cast<D*>(ctx)->BlockQueue(txn, callback, cookie);
[115] }

[201] void Queue(block_op_t* txn, block_queue_callback callback, void* cookie) const {
[202]     ops_->queue(ctx_, txn, callback, cookie);
[203] }

請注意 C++ 和 C 很類似,這是因為產生的程式碼會將 C 標頭檔案納入 C++ 標頭檔案。

交易回呼具有下列引數:

引數 意義
ctx Cookie
status 非同步回應的狀態 (由受呼叫者提供)
op 移轉中的資料

這與上述的 @banjo_layout("ddk-callback") 屬性有何不同?

首先,struct 中沒有回呼和 Cookie 值,而是會以引數形式內嵌。

其次,提供的回呼為「一次性使用」函式。 也就是說,每次叫用接收通訊協定的通訊協定方法時,都應呼叫 API 一次,而且只應呼叫一次。相反地,ddk-callback 提供的方法屬於「註冊一次,呼叫多次」的函式類型 (與 ddk-interfaceddk-protocol 類似)。因此,ddk-callbackddk-interface 結構通常會配對 register()unregister() 呼叫,以便在應停止呼叫這些回呼時通知父項裝置。

此外,@async 要注意的是,您「必須」針對每個通訊協定方法叫用呼叫其回呼,並提供隨附的 Cookie。否則會導致未定義的行為 (可能是外洩、死結、逾時或當機)。

雖然目前情況並非如此,但 C++ 和未來的語言繫結 (如 Rust) 會在產生的程式碼中提供「未來的」/「promise」樣式 (以這些回呼為基礎建構而成),藉此避免錯誤。

好的,使用 @async 還有一個注意事項:@async 屬性「只會」套用至立即下列方法,不會套用至任何其他方法。

緩衝區屬性

這個屬性適用於 vector 類型的通訊協定方法參數,用於表示這些參數會做為緩衝區使用。在實務上,這只會影響產生的參數名稱。

已分配的呼叫端屬性

套用至 vector 類型的通訊協定方法輸出參數時,該屬性會告知向量內容應由方法呼叫的接收器分配。

derive_debug 屬性 (僅限 C 繫結)

套用至列舉宣告時,系統會為 C 繫結產生輔助程式 *_to_str() 函式,該函式會為列舉的每個值傳回 const char*。例如,使用這個屬性宣告的列舉,例如

@derive_debug
enum ExampleEnum {
    VAL_ONE = 1;
    VAL_TWO = 2;
};

會產生下列產生的定義。

#ifndef FUNC_EXAMPLE_ENUM_TO_STR_
#define FUNC_EXAMPLE_ENUM_TO_STR_
static inline const char* example_enum_to_str(example_enum_t value) {
  switch (value) {
    case EXAMPLE_ENUM_VAL_ONE:
      return "EXAMPLE_ENUM_VAL_ONE";
    case EXAMPLE_ENUM_VAL_TWO:
      return "EXAMPLE_ENUM_VAL_TWO";
  }
  return "UNKNOWN";
}
#endif

inner_pointer 屬性

若是 vector 類型的通訊協定輸入參數,這個屬性會將向量的內容轉換為物件的指標,而非物件本身。

in_out 屬性

在通訊協定方法輸入參數中加入此屬性後,參數就會變成可變動,進而有效轉換成「in-out」參數。

可變動屬性

這項屬性應用於將 vectorstring 類型的 struct/union 欄位設為可變動。

命名空間屬性

這個屬性適用於 const 宣告,並讓 C 後端在常數名稱前面加上加上 snake 大小寫的 FIDL 程式庫名稱,例如 library_name_CONSTANT_K,而非 CONSTANT_K。可能需要這個屬性,以免名稱與相同建構目標中的 FIDL hlcpp 常數繫結發生衝突。

out_of_line_contents 屬性

這個屬性允許 struct/unionvector 欄位的內容儲存在容器外。

keep_c_names 屬性

這項屬性適用於 struct 宣告,並會在透過 C 後端執行時,讓欄位名稱維持不變。

班鳩琴

Banjo 會為每個通訊協定產生 C++ 模擬類別,這個模擬可傳遞給測試中的通訊協定使用者。

建築

Zircon 測試會自動取得模擬標頭。測試 Zircon 的大小必須依附於具有 _mock 後置字串的通訊協定目標,例如 //sdk/banjo/fuchsia.hardware.gpio:fuchsia.hardware.gpio_banjo_cpp_mock

使用模擬

測試程式碼必須包含通訊協定標頭和 -mock 後置字串,例如 #include <fuchsia/hardware/gpio/cpp/banjo-mock.h>

請參考以下 Banjo 通訊協定程式碼片段:

[20] @transport("Banjo")
[21] @banjo_layout("ddk-protocol")
[22] protocol Gpio {
 ...
[53]     /// Gets an interrupt object pertaining to a particular GPIO pin.
[54]     GetInterrupt(struct {
[55]         flags uint32;
[56]     }) -> (resource struct {
[57]         s zx.Status;
[58]         irq zx.Handle:INTERRUPT;
[59]     });
 ...
[82] };

以下是 Banjo 產生的模擬類別對應位元:

[034] class MockGpio : ddk::GpioProtocol<MockGpio> {
[035] public:
[036]     MockGpio() : proto_{&gpio_protocol_ops_, this} {}
[037]
[038]    virtual ~MockGpio() {}
[039]
[040]     const gpio_protocol_t* GetProto() const { return &proto_; }
 ...
[067]     virtual MockGpio& ExpectGetInterrupt(zx_status_t out_s, uint32_t flags, zx::interrupt out_irq) {
[068]         mock_get_interrupt_.ExpectCall({out_s, std::move(out_irq)}, flags);
[069]         return *this;
[070]     }
 ...
[092]     void VerifyAndClear() {
 ...
[098]         mock_get_interrupt_.VerifyAndClear();
 ...
[103]     }
 ...
[131]     virtual zx_status_t GpioGetInterrupt(uint32_t flags, zx::interrupt* out_irq) {
[132]         std::tuple<zx_status_t, zx::interrupt> ret = mock_get_interrupt_.Call(flags);
[133]         *out_irq = std::move(std::get<1>(ret));
[134]         return std::get<0>(ret);
[135]     }

MockGpio 類別會實作 GPIO 通訊協定。ExpectGetInterrupt 可用來設定對 GpioGetInterrupt 的呼叫方式。GetProto 是用來取得可傳遞至測試中程式碼的 gpio_protocol_t。這個程式碼會呼叫 GpioGetInterrupt,確保必須使用正確的引數呼叫,並傳回 ExpectGetInterrupt 指定的值。最後,測試可以呼叫 VerifyAndClear,確認是否滿足所有期望。以下是使用此模擬測試的範例:

TEST(SomeTest, SomeTestCase) {
    ddk::MockGpio gpio;

    zx::interrupt interrupt;
    gpio.ExpectGetInterrupt(ZX_OK, 0, zx::move(interrupt))
        .ExpectGetInterrupt(ZX_ERR_INTERNAL, 100, zx::interrupt());

    CodeUnderTest dut(gpio.GetProto());
    EXPECT_OK(dut.DoSomething());

    ASSERT_NO_FATAL_FAILURE(gpio.VerifyAndClear());
}

等式運算子覆寫

使用含結構類型的 Banjo 模擬測試必須定義等式運算子覆寫。舉例來說,如果是結構類型 some_struct_type,測試必須定義含有簽名的函式

bool operator==(const some_struct_type& lhs, const some_struct_type& rhs);

頂層命名空間中

自訂模擬

某些測試可能需要變更預設的模擬行為。為協助達成此目標,所有預期和通訊協定方法均為 virtual,且所有 MockFunction 成員皆為 protected

非同步方法

根據預設,Banjo 模擬會從所有非同步方法發出回呼。