web-dev-qa-db-ja.com

タイプセーフな構造体ポインタではなく、パブリックAPIでのキャストが必要な不透明な「ハンドル」を使用する理由は何ですか。

現在、パブリックAPIが次のようになっているライブラリを評価しています:

libengine.h

/* Handle, used for all APIs */
typedef size_t enh;


/* Create new engine instance; result returned in handle */
int en_open(int mode, enh *handle);

/* Start an engine */
int en_start(enh handle);

/* Add a new hook to the engine; hook handle returned in h2 */
int en_add_hook(enh handle, int hooknum, enh *h2);

enhは、いくつかの異なるデータ型(enginesおよびhooks)へのハンドルとして使用される汎用ハンドルであることに注意してください。

内部的には、これらのAPIのほとんどはもちろん、 "ハンドル"を内部構造にmallocしてキャストします。

engine.c

struct engine
{
    // ... implementation details ...
};

int en_open(int mode, *enh handle)
{
    struct engine *en;

    en = malloc(sizeof(*en));
    if (!en)
        return -1;

    // ...initialization...

    *handle = (enh)en;
    return 0;
}

int en_start(enh handle)
{
    struct engine *en = (struct engine*)handle;

    return en->start(en);
}

個人的には、typedefsの背後にあるものを非表示にするのは嫌いです。特に、タイプセーフが損なわれる場合はそうです。 (enhが与えられた場合、それが実際に何を参照しているかを知るにはどうすればよいですか?)

そこで、プルリクエストを送信し、次のAPIの変更を提案しました(entireライブラリを変更して適合させた後):

libengine.h

struct engine;           /* Forward declaration */
typedef size_t hook_h;    /* Still a handle, for other reasons */


/* Create new engine instance, result returned in en */
int en_open(int mode, struct engine **en);

/* Start an engine */
int en_start(struct engine *en);

/* Add a new hook to the engine; hook handle returned in hh */
int en_add_hook(struct engine *en, int hooknum, hook_h *hh);

もちろん、これにより内部APIの実装が格段に良くなり、キャストが不要になり、消費者の観点からの/からのタイプセーフが維持されます。

libengine.c

struct engine
{
    // ... implementation details ...
};

int en_open(int mode, struct engine **en)
{
    struct engine *_e;

    _e = malloc(sizeof(*_e));
    if (!_e)
        return -1;

    // ...initialization...

    *en = _e;
    return 0;
}

int en_start(struct engine *en)
{
    return en->start(en);
}

私は以下の理由でこれを好みます:

  • タイプセーフを追加
  • タイプとその目的の明確化の向上
  • キャストとtypedefsを削除
  • C の不透明型の推奨パターンに従います

ただし、プロジェクトの所有者はプルリクエストに失敗しました(言い換え):

個人的には、struct engine。現在の方法はよりクリーンでフレンドリーです。

最初はフックハンドルに別のデータ型を使用していましたが、次にenhを使用するように切り替えることにしました。そのため、すべての種類のハンドルは同じデータ型を共有し、単純にしています。これが紛らわしい場合、確かに別のデータ型を使用できます。

他の人がこのPRについてどう思うか見てみましょう。

このライブラリは現在非公開のベータ段階にあるため、(まだ)心配するコンシューマコードは多くありません。また、名前を少し難読化しました。


名前付きの不透明な構造体よりも不透明なハンドルの方が優れていますか?

注:この質問は Code Review で行われましたが、締め切られました。

27

「シンプルな方が良い」というマントラは、独断的になりすぎています。他のものを複雑にする場合、シンプルは必ずしも良いとは限りません。アセンブリは単純です-各コマンドは高水準言語のコマンドよりもはるかに単純ですが、アセンブリプログラムは同じことを行う高水準言語よりも複雑です。あなたの場合、均一なハンドルタイプenhは、関数を複雑にする代わりに、タイプを単純にします。通常、プロジェクトの型は関数と比較して線形に近い割合で増加する傾向があるため、プロジェクトが大きくなると、通常、関数をより単純にすることができる場合はより複雑な型を好むので、この点であなたのアプローチは正しいもののようです。

プロジェクトの作者は、あなたのアプローチが「struct engineを公開する」ことを懸念しています。私はそれが構造体自体を公開していないことを彼らに説明したでしょう存在するという事実engineという名前の構造体のみ。ライブラリのユーザーは既にそのタイプを認識している必要があります。たとえば、en_add_hookの最初の引数がそのタイプであり、最初の引数が別のタイプであることを知っている必要があります。そのため、関数の「シグネチャ」ドキュメントにこれらのタイプをドキュメント化する代わりに、どこか他の場所にドキュメント化する必要があり、コンパイラがプログラマのタイプを検証できなくなったため、実際にはAPIがより複雑になります。

注意すべきことの1つ-新しいAPIでは、ユーザーコードを作成する代わりに、少し複雑にします。

enh en;
en_open(ENGINE_MODE_1, &en);

ハンドルを宣言するには、より複雑な構文が必要です。

struct engine* en;
en_open(ENGINE_MODE_1, &en);

ただし、解決策は非常に簡単です。

struct _engine;
typedef struct _engine* engine

そして今あなたは簡単に書くことができます:

engine en;
en_open(ENGINE_MODE_1, &en);
33
Idan Arye

ここで両側に混乱があるようです:

  • ハンドルアプローチを使用しても、すべてのハンドルに単一のハンドルタイプを使用する必要はありません。
  • struct名を公開しても詳細は公開されません(存在のみ)

Cのような言語では、ベアポインターではなくハンドルを使用することには利点があります。これは、ポインターを渡すと指示先を直接操作できるため(freeの呼び出しを含む)、ハンドルを渡すとクライアントが通過する必要があるためです。任意のアクションを実行するためのAPI。

ただし、typedefを介して定義される単一のタイプのハンドルを使用するアプローチはタイプセーフではなく、多くの悲しみを引き起こす可能性があります。

したがって、私の個人的な提案は、タイプセーフハンドルに移行することです。これはかなり単純に達成されます:

typedef struct {
    size_t id;
} enh;

typedef struct {
    size_t id;
} oth;

現在では、誤って2をハンドルとして渡したり、エンジンへのハンドルが必要なほうきにハンドルを誤って渡したりすることはできません。


そこで、プルリクエストを送信し、次のAPIの変更を提案しました(entireライブラリを変更して適合させた後)。

それはあなたの間違いです:オープンソースライブラリの重要な作業に従事する前に、変更について議論するために作者/メンテナに連絡してくださいpfront。これにより、あなたは何をすべきか(あるいは何をしないか)に同意でき、不必要な作業とそれに起因するフラストレーションを回避できます。

16
Matthieu M.

不透明なハンドルが必要な状況は次のとおりです。

struct SimpleEngine {
    int type;  // always SimpleEngine.type = 1
    int a;
};

struct ComplexEngine {
    int type;  // always ComplexEngine.type = 2
    int a, b, c;
};

int en_start(enh handle) {
    switch(*(int*)handle) {
    case 1:
        // treat handle as SimpleEngine
        return start_simple_engine(handle);
    case 2:
        // treat handle as ComplexEngine
        return start_complex_engine(handle);
    }
}

上記の「タイプ」のように、ライブラリにフィールドのヘッダー部分が同じ2つ以上の構造体タイプがある場合、これらの構造体タイプは共通の親構造体(C++の基本クラスなど)を持つと見なすことができます。

このように、ヘッダー部分を「構造エンジン」として定義できます。

struct engine {
    int type;
};

struct SimpleEngine {
    struct engine base;
    int a;
};

struct ComplexEngine {
    struct engine base;
    int a, b, c;
};

int en_start(struct engine *en) { ... }

ただし、構造体エンジンの使用に関係なく型キャストが必要になるため、これはオプションの決定です。

結論

場合によっては、不透明な名前付き構造体の代わりに不透明なハンドルが使用される理由があります。

3
Akio Takahashi

ハンドルアプローチの最も明白な利点は、外部APIを壊すことなく内部構造を変更できることです。確かに、クライアントソフトウェアを変更する必要はありますが、少なくともインターフェイスは変更していません。

それが行うもう1つのことは、それぞれに明示的なAPIインターフェースを提供する必要なしに、実行時に多くの異なるタイプから選択する機能を提供することです。各センサーがわずかに異なり、わずかに異なるデータを生成するいくつかの異なるセンサータイプからのセンサー測定値などの一部のアプリケーションは、このアプローチによく応答します。

とにかくクライアントに構造を提供することになるので、キャストが必要なAPIであっても、はるかに単純なAPIの場合は型の安全性を少し犠牲にします(まだ実行時にチェックできます)。

2
Robert Harvey

デジャヴ

名前付きの不透明な構造体よりも不透明なハンドルの方が優れていますか?

exactと同じシナリオに遭遇しましたが、わずかな違いがあります。 SDKには、次のようなものがたくさんありました。

typedef void* SomeHandle;

私の単なる提案は、それを私たちの内部タイプと一致させることでした:

typedef struct SomeVertex* SomeHandle;

SDKを使用するサードパーティにとっては、何の違いもありません。不透明なタイプです。誰も気にしない?これは、ABI *またはソースの互換性に影響を与えません。SDKの新しいリリースを使用するには、プラグインを再コンパイルする必要があります。

* gnasherが指摘しているように、structやvoid *へのポインターのようなもののサイズが実際には異なるサイズである場合があることに注意してください。その場合、ABIに影響します。彼のように、私は実際にそれに遭遇したことがありません。しかし、その観点からすると、2番目の方法は実際には一部のあいまいなコンテキストでの移植性を向上させる可能性があるため、2番目の方法を好むもう1つの理由です。

サードパーティのバグ

さらに、内部開発/デバッグのタイプセーフ以上の原因がありました。 2つの類似したハンドル(PanelPanelNew、つまり)が両方ともvoid* typedefをハンドルに使用していたため、コードにバグがあったプラグイン開発者がすでにたくさんいました。すべてに対してvoid*を使用した結果、誤ったハンドルを誤った場所に誤って渡す。そのため、実際にはSDKを使用する側でバグが発生していました。彼らのバグは、SDKのバグについて苦情を報告するバグレポートを送信するため、内部開発チームにも多大な時間を要します。プラグインをデバッグし、プラグインのバグが間違ったハンドルを渡したことが原因であることがわかります。間違った場所へ(これは、すべてのハンドルがvoid*またはsize_tのエイリアスである場合、警告なしでも簡単に許可されます)。したがって、単なるでさえ、すべての内部情報を隠蔽するという概念の純粋さを求める私たちの望みによって彼らの側でミスが発生したため、サードパーティにデバッグサービスを提供する時間を無駄に無駄にしていた私たちの内部structsの。

Typedefを保持

違いは、クライアントにstruct SomeVertexを書き込ませずにtypedefに固執することを提案していたことです。これは将来のソース互換性に影響を与えますプラグインのリリース。私は個人的にはCでstructをタイプ定義しないというアイデアが好きですが、SDKの観点からは、typedefは全体のポイントが不透明であるため役立ちます。したがって、公開されているAPIのためだけにこの標準を緩和することをお勧めします。 SDKを使用しているクライアントにとって、ハンドルが構造体へのポインタであるか整数であるかは問題ではありません。重要なのは、2つの異なるハンドルが同じデータ型にエイリアスを設定しないことです。間違ったハンドルを間違った場所に渡します。

タイプ情報

キャストを避けることが最も重要なのは、内部開発者であるあなたです。 SDKからすべての内部を隠すというこのような美的感覚は、すべてのタイプ情報を失うという多大な犠牲を伴う概念的な美観であり、不必要にキャストを散布する必要があります。重要な情報を取得するデバッガ。 CプログラマーはCでこれに慣れているはずですが、これを不必要に要求することは問題を求めるだけです。

概念的な理想

一般的に、純粋であるという概念的な考えを、すべての実用的な日常のニーズよりもはるかに優先するタイプの開発者に注意する必要があります。これらは、ユートピアの理想を模索する際にコードベースの保守性を徹底的に推進し、チーム全体が砂漠では日焼けローションを避けます。それは不自然であり、乗組員の半分が皮膚ガンで死んでいる間にビタミンD欠乏症を引き起こす可能性があるからです。

ユーザーエンド設定

APIを使用するユーザーの厳密なユーザーエンドの観点からでも、バグのあるAPIまたはうまく機能するAPIを好むでしょうか?それは実際的なトレードオフだからです。一般的なコンテキストの外で不必要に型情報を失うと、バグのリスクが高まり、長年にわたるチーム全体の設定の大規模なコードベースから、マーフィーの法則はかなり適用される傾向があります。バグのリスクを過度に増加させると、少なくともいくつかのバグが発生する可能性があります。大規模なチーム環境では、考えられるあらゆる種類の人為的ミスが最終的には可能性から現実に移行することを発見するのにそれほど長くはかかりません。

したがって、おそらくそれはユーザーに提起する問題です。 「バグの多いSDKか、気にすることのない内部の不透明な名前を公開するSDKのどちらを好みますか?」そして、もしその質問が誤った二分法を提示しているようであれば、バグのリスクが高くなると最終的には実際のバグが長期的に現れるという事実を理解するには、非常に大規模な環境でチーム全体の経験が必要になると思います。開発者がバグを回避することにどれだけ自信を持っているかは重要ではありません。チーム全体の設定では、最も弱いリンクについて、そして少なくともそれらがつまずくことを防ぐための最も簡単で迅速な方法について考えることは、より役立ちます。

提案

だから私はここに妥協を提案します、それでもあなたにすべてのデバッグの利点を保持する能力を与えます:

typedef struct engine* enh;

... structをtypedefする犠牲を払っても、本当に私たちを殺しますか?おそらくそうではないので、私もあなたの側でいくつかの実用主義をお勧めしますが、ここでsize_tを使用し、さらに非表示にする以外に正当な理由なしに整数にキャスト/キャストすることでデバッグを指数関数的に困難にすることを望む開発者にはさらに推奨しますユーザーには既に99%隠されており、size_tよりも害を及ぼす可能性のない情報。

2
user204677

本当の理由は慣性だと思う。それは彼らがいつもやってきたことであり、それが機能するので、なぜそれを変えるのか?

私が見ることができる主な理由は、不透明なハンドルにより、デザイナーが構造体だけでなく、背後に何でも置くことができるためです。 APIが複数の不透明なタイプを返し、受け入れる場合、それらはすべて呼び出し元に同じように見え、細かい印刷が変更された場合にコンパイルの問題や再コンパイルが必要になることはありません。 en_NewFlidgetTwiddler(handle ** newTwiddler)が変更されて、ハンドルではなくTwiddlerへのポインターを返す場合、APIは変更されず、新しいコードはハンドルを使用する前のポインターを暗黙的に使用します。また、ポインターが境界を越えても、OSやその他の静かなポインターの「修正」の危険はありません。

もちろん、その不利な点は、発信者が何でもそれにフィードできることです。あなたは64ビットのものを持っていますか?それをAPI呼び出しの64ビットスロットに押し込み、何が起こるかを確認します。

en_TwiddleFlidget(engine, twiddler, flidget)
en_TwiddleFlidget(engine, flidget, twiddler)

両方ともコンパイルされますが、どちらか一方だけがあなたの望みどおりの動作をするはずです。

1
Móż

この態度は、CライブラリAPIを初心者による乱用から守るという長年の哲学に由来していると思います。

特に、

  • ライブラリの作成者は、それが構造体へのポインタであることを知っており、構造体の詳細はライブラリコードに表示されます。
  • seであるすべての経験豊富なプログラマーは、ライブラリがそれがいくつかの不透明な構造体へのポインターであることも知っています;
    • 彼らは十分なハードな苦痛知っておくべき経験バイトを台無しにしないことそれらの構造体に格納されていました。
  • 経験の浅いプログラマはどちらも知らない。
    • それらは、不透明なデータをmemcpyしようとするか、構造体内のバイトまたはワードをインクリメントしようとします。ハッキングに行きます。

長年の伝統的な対策は次のとおりです。

  • 不透明なハンドルが、実際には同じプロセスメモリ空間に存在する不透明な構造体へのポインタであるという事実を偽装します。
    • これを行うには、それを_void*_と同じビット数の整数値であると主張します。
    • さらに慎重にするために、ポインタのビットも偽装します。
      struct engine* peng = (struct engine*)((size_t)enh ^ enh_magic_number);

これは、長い伝統があると言っているだけです。それが正しいかどうかについて私は個人的な意見を持っていませんでした。

1
rwong