C 呼叫 C++ 函式的方法

由於 C 與 C++ 基於歷史因素、互相相容、以及地位相近的關係, 他們的程式碼很有機會會在一個專案中並存。 但畢竟兩者的差異還是有點大,於是在互相使用對方的東西的時候,有時候會遇到一些技術上的小麻煩, 這裡我就來解釋有關 C 語言程式碼要如何呼叫 C++ 函式的相關技巧。

首先,為什麼我不關心 C++ 要如何呼叫 C 函式?這是因為這種案例幾乎不存在問題。 C++ 號稱是 C 的超集,C 有的東西絕大部分都被 C++ 支援,不用特別改變什麼, 最多就是把 name mangling 的部份小小宣告修飾一下就完事了,實在沒什麼好說的。 相反的,C++ 多出很多 C 所沒有的語法等特性, 所以在 C 一般來說是沒辦法去呼叫沒有經過一些特別處理的 C++ 函式的。

概念原則

C 與 C++ 基本上很多機制都還是完全相容的, 而 C 呼叫 C++ 函式所遭遇的困難追根究底其實就是以下兩點:

  1. Name mangling 規則不同,所以無法找到 C++ 函式的符號。
  2. 函式介面出現一些 C 所沒有支援的語法, 使得 C 編譯器無法辨認函式簽名、C 程式碼也無法提供所需的函式參數或呼叫方式。

那麼解決的方法就是讓 C++ 模組提供一些符合純 C 要求的函式介面給 C 程式碼呼叫即可。 這裡注意我們不是要把 C++ 程式碼整個以純 C 語法改寫, 如果這樣做的話,又和苦用 C++ 來寫模組呢? 而且這樣的話,也無需使用 C++ 編譯器,那麼本文所探討的問題也就不存在了。 我們只需要在「C 編譯器看得到的地方」使用完全符合 C 語法的內容, 也就是只需提供符合 C 語法的函式介面,必要的時候增加一些包裝函式來轉換某些內容即可!

呼叫單純普通的 C++ 函式

假設我有一個 C++ 模組,裡面有一個函式:

int AddValues(int a, int b)
{
    return a + b;
}

若在 C 程式裡呼叫它的話,可能會發生「undefined reference」連結錯誤, 這是因為 C 和 C++ name mangling 不同的緣故,所以找不到函式符號。 解決的方法就是加上 extern "C" 來告訴 C++ 編譯器說這是一個 C 規格的函式, 讓 C++ 編譯器使用 C 的規則來看待它。 最後這個函式應該會變成下面這樣:

標頭檔裡:

// 因為 C 並沒有 extern "C" 這個關鍵字,在 C 編譯時反而會發生問題,
// 所以放了 __cplusplus 保護,好讓 C 編譯器看不到 extern "C" 的存在。
#ifdef __cplusplus
extern "C" {
#endif

// 這裡可能還有很多別的函式宣告、型態宣告等等等等……

int AddValues(int a, int b);

// 這裡可能還有很多別的函式宣告、型態宣告等等等等……

#ifdef __cplusplus
}   // extern "C"
#endif

程式碼檔裡:

// 這裡一般就不用再寫上 extern "C",因為編譯器已經從函式宣告那裡知道了這件事。
int AddValues(int a, int b)
{
    return a + b;
}

這樣,C 程式就可以順利的呼叫這個函式了。

此外,使用 C 規則來處理函式就意味著一些 C++ 所支持的特性是無法被使用在這些 C 介面函式的, 若一不小心使用到這些語法特性,則可能會發生編譯錯誤。 這些無法被使用的特性有:

  • 同名函式重載(function overload)
  • 模版(template)
  • 名稱空間(namespace)

呼叫包含常見 C++ 擴展型態參數的 C++ 函式

在解決函式符號問題後,接下來會遇到的困難大概就是那些 在 C++ 程式裡經常出現的語法或經常傳遞的 C++ 物件了,比如說字串等。 這些東西都需要改寫為 C 所支援的寫法:

  • 預設參數

    C 不支援預設參數,請把它拿掉,然後每次呼叫的時候都老老實實的寫上全部的參數吧!

  • 參考傳遞

    C++ 的參考型態函式參數可以節省許多賦值傳遞的開銷, C 雖沒有參考型態,但有作用非常類似的指標,因此請把傳遞參考改成傳遞指標吧。

  • 字串與陣列

    嚴格來說,C++ 也沒有語法上的字串或陣列,只有標準程式庫提供的字串類別, 但是因為有些陣列、特別是字串實在是太常以類別物件的型態出現在函式參數裡了, 所以還是值得說明。 C 函式只能傳入指標型態的陣列, 所以請將參數裡的 std::string&const std::string& 通通改成 char*const char* 吧, 其他型態的陣列也請比照進行類似的變換。 如果需要傳出字串或陣列資料,則請比照 C 語言裡的常用通用方法!

如果你能夠改寫 C++ 函式的介面規格的話就直接改寫吧,這樣可以省去一個轉呼函式的開銷, 除了當別的 C++ 程式呼叫這個 C++ 函式時可能會稍微煩瑣一點以外。 而如果因為任何緣故不能夠直接改寫這個函式介面,那就需要再寫個包裝轉呼函式了, 下面以一個範例來說明使用轉呼函式包裝一個 C++ 函式的方法:

標頭檔裡:

#ifdef __cplusplus

// 這是我們想要給 C 程式呼叫的 C++ 函式,
// 因為用了 __cplusplus 包裹起來,所以 C 編譯器是看不到它的。
int ShowName(const std::string &name, const std::string &family = "");

#endif  // __cplusplus

#ifdef __cplusplus
extern "C" {
#endif

// 這是實際提供給 C 程式的函式,用來包裝我們的 ShowName 函式。
// 注意我變換了名稱,以規避同名函式重載的問題。
int show_name(const char *name, const char *family);

#ifdef __cplusplus
}   // extern "C"
#endif

程式碼檔裡:

int ShowName(const std::string &name, const std::string &family = "")
{
    std::string totalname = name + ( family.empty() ? "" : " " ) + family;
    std::cout << "You are: " << totalname << std::endl;
    return totalname.length();
}

int show_name(const char *name, const char *family)
{
    return ShowName(name, family);
}

呼叫包含類別型態參數的 C++ 函式

有的時候我們的函式就是需要傳遞一些類別物件,這些物件可能是自訂的也可能是標準程式庫裡的, 而且這些物件可能不像字串那樣有標準通用的簡單等效形式,那要怎麼辦呢? 簡單的答覆就是,傳遞物件的指標就好了! C 語言支援指標,而指標的另一端可以是任何型態,就算是未知的型態也無妨。

我們可以用指標型態簡單的解決參數傳遞問題, 所以真正的問題不在物件的傳遞上,而是在類別物件的擁有與管理上。 既然 C 不支援 C++ 的類別,又如何能夠知道類別的尺寸大小、該如何初始化、以及該如何銷毀等細節呢? 所以我們會需要同步包裝一些類別物件的建立與銷毀相關函式給 C 使用, 總之重點原則就是: C 只能透過指標經手並傳遞類別物件。 下面以包裝一個需要接收 std::map 型態菜單的餐點點單函式做為例子:

標頭檔裡:

#ifdef __cplusplus

// 這是我們主要要包裝的函式。
void OrderDinner(const std::map<std::string,unsigned> &menu);

#endif  // __cplusplus

#ifdef __cplusplus
extern "C" {
#endif

// 這裡為了節省一些其他的型態相關處理敘述,直接使用 void* 做為菜單物件的型態,
// 但在實際用途上我們應該為它定義一個合適的型態,
// 除了增加閱讀性以外,也讓編譯器能為我們進行型態檢查。
void order_dinner(const void *menu);

// 因為 C 程式無法直接創建與使用類別物件,
// 所以要同時提供讓 C++ 程式代為執行這些工作的包裝函式。
void* create_my_menu(unsigned appetizer, unsigned main, unsigned dessert);
void release_my_menu(void *menu);

#ifdef __cplusplus
}   // extern "C"
#endif

程式碼檔裡:

void OrderDinner(const std::map<std::string,unsigned> &menu)
{
    std::cout << "Your order list:" << std::endl;
    for(auto iter = menu.cbegin(); iter != menu.cend(); ++iter)
        std::cout
            << "* "
            << iter->first
            << ": "
            << iter->second
            << std::endl;
}

void order_dinner(const void *menu)
{
    // 因為我們沒有額外處理型態,只簡單的用 void* 代表一切,
    // 所以這裡免不了需要一些比較難看的轉型工作,下面亦同。
    auto *instance =
        static_cast<const std::map<std::string,unsigned>*>(menu);

    OrderDinner(*instance);
}

void* create_my_menu(unsigned appetizer, unsigned main, unsigned dessert)
{
    auto menu = new std::map<std::string,unsigned>;

    (*menu)["appetizer"] = appetizer;
    (*menu)["main"] = main;
    (*menu)["dessert"] = dessert;

    return menu;
}

void release_my_menu(void *menu)
{
    auto *instance =
        static_cast<std::map<std::string,unsigned>*>(menu);

    delete instance;
}

而在 C 程式裡,它可能會被這樣使用:

void *menu = create_my_menu(3, 5, 7);

order_dinner(menu);

release_my_menu(menu);

呼叫 C++ 類別成員函式

由於 C 沒有類別,自然的也不知如何呼叫類別成員函式。 而解決方法也很簡單,按照前面的思路為所有需要的成員函式提供轉呼函式包裝即可, 這裡直接看範例。

標頭檔裡:

#ifdef __cplusplus

// (提醒您,這個地方只有 C++ 看得到)
// 這是我們要提供給 C 程式使用的類別,包含幾個簡單的成員函式。
class Point
{
private:
    float x, y;

public:
    Point(float x, float y) : x(x), y(y) {}

public:
    void Set(float x, float y) { this->x = x; this->y = y; }
    float Abs() const { return sqrt(x*x + y*y); }
};

#else   // __cplusplus

// (提醒您,這個地方只有 C 看得到)
// 這裡是一個小技巧,在 C 程式下定義一個與我們的類別同名的結構別名。
// 至於結構的具體內容不重要,因為我們在 C 程式下只需要使用它的指標型態而已,
// 而與類別同名,可以讓我們的 C++ 程式不需要一堆醜陋的轉型。
typedef struct Point Point;

#endif  // __cplusplus

#ifdef __cplusplus
extern "C" {
#endif

// 與前面的例子相同,我們亦需同步提供取得、和歸還類別物件的方法。
Point* point_create(float x, float y);
void point_release(Point *self);

// 注意這裡又是一個小技巧,既然這些函式是成員函式的包裝,
// 我把類別物件的指標命名為 self 來類比 C++ 的 this 指標
// (因為 this 是 C++ 關鍵字而不能使用),
// 並且我還把類別物件的指標都設為函式的第一個參數,以相似 C++ 成員函式的慣例。
// 雖然這些小地方的設定並不是必要的,但這麼做確實有助於閱讀理解,
// 特別是對於那些本來就熟悉 C++ 的人。
void point_set(Point *self, float x, float y);
float point_abs(const Point *self);

#ifdef __cplusplus
}   // extern "C"
#endif

程式碼檔裡:

Point* point_create(float x, float y)
{
    return new Point(x, y);
}

void point_release(Point *self)
{
    delete self;
}

void point_set(Point *self, float x, float y)
{
    self->Set(x, y);
}

float point_abs(const Point *self)
{
    return self->Abs();
}

而在 C 程式裡,它可能會被這樣使用:

Point *point = point_create(3, 7);

point_set(point, 2, 6);
printf("Point abstract value: %f\n", point_abs(point));

point_release(point);