驅動程式與驅動程式管理器如何協同運作

注意

本文著重於實作或使用 adbc.h 中 C API 定義的驅動程式/應用程式。這包含 C/C++、Python 和 Ruby;以及可能包含 C#、Go 和 Rust(當透過 FFI 實作或使用驅動程式時)。

當應用程式呼叫像 AdbcStatementExecuteQuery() 這樣的函數時,它如何「知道」實際上要呼叫哪個驅動程式中的哪個函數?

這可以透過幾種方式發生。在最簡單的情況下,應用程式連結到單一驅動程式,並直接呼叫驅動程式明確定義的 ADBC 函數

../_images/DriverDirectLink.mmd.svg

在最簡單的情況下,應用程式直接連結到驅動程式並呼叫 ADBC 函數。

這不適用於多個驅動程式,或不/無法直接連結到驅動程式的應用程式(想想動態載入,可能在像 Python 這樣的語言中)。對於這種情況,ADBC 提供了一個函數指標表 (AdbcDriver),以及一種從驅動程式請求此表格的方式。然後,應用程式分兩個步驟進行。首先,它動態載入驅動程式並呼叫入口點函數以取得函數表

../_images/DriverTableLoad.mmd.svg

現在,應用程式向驅動程式請求要呼叫的函數表。

然後,應用程式透過呼叫表中的函數來使用驅動程式

../_images/DriverTableUse.mmd.svg

應用程式使用表格來呼叫驅動程式函數。這種方法適用於多個驅動程式。

然而,處理表格很繁瑣。因此,總體推薦的方法是使用 ADBC 驅動程式管理器。這是一個函式庫,它偽裝成可以連結和「像正常一樣」使用的單一驅動程式。在內部,它載入函數指標表並追蹤哪些資料庫/連線/語句物件需要哪個「實際」驅動程式,使其易於在執行階段動態載入驅動程式,並從同一個應用程式中使用多個驅動程式

../_images/DriverManagerUse.mmd.svg

應用程式使用驅動程式管理器來「感覺像」它只是在使用單一驅動程式。驅動程式管理器在幕後處理細節。

更詳細說明

adbc.h 標頭檔將所有事物整合在一起。它是抽象 API 定義,類似於其他語言中的介面/特徵/協定定義。然而,C 語言本質上,它僅由一堆函數原型和結構定義組成,而沒有任何實作。

驅動程式的核心來說,只是一個實作 adbc.h 中那些函數原型的函式庫。這些函數可以用 C 語言實作,也可以用不同的語言實作,並透過語言特定的 FFI 機制匯出。例如,ADBC 的 Go 和 C# 實作都可以向期望 C API 定義的使用者匯出驅動程式。只要 adbc.h 中的定義以某種方式實作,那麼應用程式通常不會察覺到實際上底層是什麼。

但是,應用程式如何呼叫這些函數呢?在這裡,有幾種選項。

再次強調,最簡單的情況如下:如果 (1) 應用程式直接連結到驅動程式,且 (2) 驅動程式以與 adbc.h 中相同的名稱公開 ADBC 函數,那麼應用程式就可以 #include <arrow-adbc/adbc.h> 並直接呼叫 AdbcStatementExecuteQuery(...)。在這裡,應用程式和驅動程式之間的關係與任何其他 C 函式庫沒有不同。

../_images/DriverDirectLink.mmd.svg

在最簡單的情況下,應用程式直接連結到驅動程式並呼叫 ADBC 函數。當應用程式呼叫 StatementExecuteQuery 時,那是直接由它連結的驅動程式提供的。

不幸的是,這在其他情況下不太適用。例如,如果應用程式希望使用多個 ADBC 驅動程式,這就不再適用:兩個驅動程式都定義相同的函數(adbc.h 中的那些函數),並且當應用程式連結它們兩者時,連結器無法分辨當應用程式呼叫 ADBC 函數時意指哪個驅動程式的函數。除此之外,這也違反了單一定義原則

在這種情況下,驅動程式可以提供應用程式可以使用的驅動程式特定別名,例如 PostgresqlStatementExecuteQueryFlightSqlStatementExecuteQuery。然後,應用程式可以連結兩個驅動程式,忽略 Adbc… 函數(並忽略那裡技術性違規的單一定義原則),並改用別名。

../_images/DriverAlias.mmd.svg

為了繞過單一定義原則,我們可以改為提供 ADBC API 的別名。

然而,這對應用程式來說相當不方便。此外,這某種程度上失去了使用 ADBC 的意義,因為現在應用程式為每個驅動程式都有單獨的 API,即使它們在技術上都是相同 API 的克隆。並且這沒有解決應用程式想要動態載入驅動程式的問題。例如,Python 腳本會想要在執行階段載入驅動程式。在這種情況下,它需要知道驅動程式中的哪些函數對應於 ADBC API 定義中的哪些函數,而無需硬編碼此知識。

ADBC 預料到這一點,並定義了 AdbcDriver。這只是一個函數指標表,每個 ADBC 函數一個條目。這樣一來,應用程式可以動態載入驅動程式,並呼叫一個入口點函數,該函數返回此函數指標表。(它確實需要硬編碼或猜測入口點的名稱;ADBC 規範列出了一組它可以嘗試的名稱,基於驅動程式函式庫本身的名稱。)

../_images/DriverTableLoad.mmd.svg

應用程式首先從驅動程式載入函數指標表。

然後,它可以透過呼叫該表中的函數來使用驅動程式

../_images/DriverTableUse.mmd.svg

應用程式使用表格來呼叫驅動程式函數。這種方法適用於多個驅動程式。

當然,通過跳過一個巨大的函數指標表來呼叫所有函數是不方便的。因此,ADBC 提供了「驅動程式管理器」,一個偽裝成簡單驅動程式並實作所有 ADBC 函數的函式庫。在內部,它動態載入驅動程式,請求函數指標表,並追蹤哪些連線正在使用哪些驅動程式。應用程式只需要呼叫標準 ADBC 函數,就像我們最初開始的最簡單情況一樣

../_images/DriverManagerUse.mmd.svg

應用程式使用驅動程式管理器來「感覺像」它只是在使用單一驅動程式。驅動程式管理器在幕後處理細節。

因此,總結一下,驅動程式應該實作這三件事

  1. 每個 ADBC 函數的實作,

  2. 每個實作函數周圍的薄包裝器,為每個函數匯出 ADBC 名稱,以及

  3. 一個入口點函數,該函數返回一個 AdbcDriver 表,其中包含來自 (1) 的函數。

然後,應用程式有以下幾種使用驅動程式的方式選擇

  • 直接連結驅動程式並呼叫 Adbc… 函數(僅在最簡單的情況下)使用上面的 (2),

  • 直接/動態連結驅動程式,透過上面的 (3) 載入 AdbcDriver,並通過函數指標呼叫 ADBC 函數(通常不建議),

  • 連結 ADBC 驅動程式管理器,呼叫 Adbc… 函數,並讓驅動程式管理器處理上面的 (3)(大多數應用程式會想要做的)。

換句話說,通常總是使用驅動程式管理器是最容易的。但它所施展的魔法並非必要或非常複雜。

注意

你可能會問:當我們有 AdbcDriver 時,為什麼我們要費心同時定義 AdbcStatementExecuteQuerySqliteStatementExecuteQuery(即,為什麼要同時做上面的 (1) 和 (2))?我們難道不能只定義 Adbc… 版本,並在請求時將其放入函數表中嗎?

在這裡,實作限制出現。在執行階段,當驅動程式查找(例如)AdbcStatementExecuteQuery 的地址以將其放入表格中時,動態連結器將發揮作用,以找出此函數的位置。不幸的是,它可能會在驅動程式管理器中找到它。這是一個問題,因為當驅動程式管理器去呼叫函數的「驅動程式」版本時,它最終會陷入無限循環!

通過擁有一個看似多餘的函數副本,然後,我們可以從動態連結器中隱藏「真實實作」,並避免這種行為。

驅動程式管理器可以嘗試通過使用 RTLD_DEEPBIND 載入驅動程式來解決這個問題。然而,這不具備可移植性,並且如果我們也想在開發期間使用像 AddressSanitizer 這樣的東西,就會造成問題。驅動程式也可以使用像 -Bsymbolic-functions 這樣的標誌進行構建。