web-dev-qa-db-ja.com

パックされた構造体は移植可能ですか?

Cortex-M4マイクロコントローラー上にいくつかのコードがあり、バイナリプロトコルを使用してPCと通信したいのですが。現在、GCC固有のpacked属性を使用して、パックされた構造体を使用しています。

大まかな概要は次のとおりです。

struct Sensor1Telemetry {
    int16_t temperature;
    uint32_t timestamp;
    uint16_t voltageMv;
    // etc...
} __attribute__((__packed__));

struct TelemetryPacket {
    Sensor1Telemetry tele1;
    Sensor2Telemetry tele2;
    // etc...
} __attribute__((__packed__));

私の質問は:

  • MCUとクライアントアプリのTelemetryPacket構造体にまったく同じ定義を使用すると仮定すると、上記のコードは複数のプラットフォーム間で移植可能になりますか? (x86とx86_64に興味があり、Windows、Linux、OS Xで実行する必要があります。)
  • 他のコンパイラは同じメモリレイアウトのパック構造体をサポートしていますか?どんな構文で?

[〜#〜] edit [〜#〜]

  • はい、私はパックされた構造体が非標準であることを知っていますが、それらを使用することを検討するのに十分役立つようです。
  • CとC++の両方に興味がありますが、GCCがそれらを異なる方法で処理するとは思いません。
  • これらの構造体は継承されず、何も継承しません。
  • これらの構造体には、固定サイズの整数フィールド、および他の同様のパック構造体のみが含まれます。 (私は前にフロートで焼かれました...)
40
Venemo

メモリに対して、コンパイルドメイン全体で構造体を使用しないでください(ハードウェアレジスタ、ファイルから読み取ったアイテムを選択する、またはプロセッサ間または同じプロセッサの異なるソフトウェア間(アプリとカーネルドライバー間)でデータを渡す)。コンパイラーはアライメントを選択する自由があるため、その上にいるユーザーが修飾子を使用することで悪化させる可能性があるため、問題を求めています。

たとえば、異なるターゲット(コンパイラの異なるビルドとターゲットの違い)に対して同じgccコンパイラバージョンを使用している場合でも、プラットフォーム間でこれを安全に実行できると仮定する理由はありません。

失敗の可能性を減らすには、最初に最大のアイテム(64ビット、32ビット、16ビット、最後に8ビットのアイテム)から始めます。理想的には、アームとx86が期待する32の最小64に合わせますが、ソースからコンパイラをビルドする人はだれでもデフォルトを変更できます。

これがジョブセキュリティの問題であれば、このコードを定期的にメンテナンスできます。各ターゲットの各構造の定義が必要になる可能性があります(したがって、ARMとx86の別の、またはすぐにではなく最終的にこれが必要になります)。そして、コードの作業を行うために呼び出されるすべての製品リリースまたはすべての製品リリース...

同じまたは異なるアーキテクチャのコンパイルドメインまたはプロセッサ間で安全に通信する場合は、あるサイズの配列、バイトストリーム、ハーフワードのストリーム、またはワードのストリームを使用します。障害やメンテナンスのリスクを大幅に減らします。構造を使用して、リスクと障害を回復するだけのアイテムを分解しないでください。

言語のルールと実装定義領域がどこにあるかを理解しているので、同じターゲットまたはファミリー(または他のコンパイラー選択から派生したコンパイラー)に対して同じコンパイラーまたはファミリーを使用しているため、人々がこれを大丈夫だと思う理由最終的に違いに出くわすこともあれば、キャリアで数十年かかることもあれば、数週間かかることもあります...「私のマシンで動作する」問題です...

8
old_timer

前述のプラットフォームを考慮すると、はい、パックされた構造体は使用するのに完全に問題ありません。 x86およびx86_64は常に非境界整列アクセスをサポートし、一般的な考えに反して、これらのプラットフォームでの非境界整列アクセスは、長時間の境界整列アクセスと同じ速度を( almost )しています(非境界整列アクセスが存在することはありません)はるかに遅い)。唯一の欠点は、アクセスがアトミックではない可能性があることですが、この場合は重要ではないと思います。コンパイラー間で合意があり、パックされた構造体は同じレイアウトを使用します。

GCC/clangは、あなたが言及した構文でパックされた構造体をサポートします。 MSVCには#pragma pack、これは次のように使用できます。

#pragma pack(Push, 1)
struct Sensor1Telemetry {
    int16_t temperature;
    uint32_t timestamp;
    uint16_t voltageMv;
    // etc...
};
#pragma pack(pop)

2つの問題が発生する可能性があります。

  1. エンディアンネスはプラットフォーム間で同じでなければなりません(MCUはリトルエンディアンを使用している必要があります)
  2. パックされた構造体メンバーにポインターを割り当て、アライメントされていないアクセスをサポートしないアーキテクチャーにいる場合(または、movapsldrdなどのアライメント要件を持つ命令を使用する場合)、そのポインタを使用するとクラッシュする可能性があります(gccはこれについて警告しませんが、clangは警告します)。

GCCのドキュメントは次のとおりです。

パックされた属性は、変数または構造体フィールドが可能な限り最小のアライメント(変数に対して1バイト)を持つことを指定します

したがって、GCCは、パディングが使用されないことを保証します

MSVC:

クラスをパックするということは、そのメンバーをメモリ内で順番に配置することです。

したがって、MSVCは、パディングが使用されないことを保証します

私が見つけた唯一の「危険な」領域は、ビットフィールドの使用です。その場合、レイアウトはGCCとMSVCで異なる場合があります。ただし、GCCには互換性があるオプションがあります:-mms-bitfields


ヒント:たとえこのソリューションが現在動作し、動作しなくなる可能性が非常に低い場合でも、このソリューションへのコードの依存度を低く保つことをお勧めします。

注:この回答では、GCC、clang、およびMSVCのみを検討しました。多分コンパイラがありますが、これらのことは真実ではありません。

25
geza

もし

  • エンディアンネス は問題ではありません
  • 両方のコンパイラがパッキングを正しく処理します
  • 両方のC実装の型定義は正確です(標準に準拠)。

はい、「packed structure」は移植可能です。

私の好みの「if」が多すぎるため、これをしないでください。手間をかける価値はありません。

12
alk

それを行うか、より信頼性の高い代替手段を使用できます。

シリアル化狂信者の中のハードコアには、 CapnProto があります。これにより、ネイティブ構造に対処することができ、ネットワークを介して転送されて軽く作業されても、もう一方の意味を理解できるようになります。それをシリアル化と呼ぶことはほぼ不正確です。構造のメモリー内表現をできる限り少しすることを目指しています。 M4への移植に適している場合があります

Google Protocol Buffersがあります。これはバイナリです。もっと膨らみますが、かなり良いです。付随するnanopb(マイクロコントローラーにより適しています)はありますが、GPB全体を実行するわけではありません(oneofを実行するとは思わない)。しかし、多くの人々はそれをうまく使います。

C asn1ランタイムの一部は、マイクロコントローラーで使用するのに十分なサイズです。 これ はM0に適合します。

8
bazza

最大限に移植可能なものが必要な場合は、uint8_t[TELEM1_SIZE]memcpy()のバッファーをその中のオフセットとの間で宣言し、htons()htonl()などのエンディアン変換を実行できます。 glibのようなエンディアンの同等物)。これをC++のgetter/setterメソッドを使用してクラスにラップするか、Cのgetter-setter関数を使用して構造体をラップできます。

2
Davislor

代替案について話し、あなたの質問を検討してください パックドデータ用のタプルのようなコンテナ (私はコメントするのに十分な評判がありません)アレックス・ロベンコの CommsChampion プロジェクトを見てください:

COMMSはC++(11)ヘッダーのみのプラットフォームに依存しないライブラリであり、通信プロトコルの実装を簡単で比較的迅速なプロセスにします。カスタムメッセージの定義を作成するために必要なすべてのタイプとクラスを提供し、トランスポートデータフィールドをラップして、タイプとクラス定義の単純な宣言ステートメントにします。これらのステートメントは、実装する必要があるものを指定します。 COMMSライブラリ内部はHOW部分を処理します。

Cortex-M4マイクロコントローラで作業しているので、次のこともおもしろいかもしれません。

COMMSライブラリは、ベアメタルシステムを含む組み込みシステムで使用するために特別に開発されました。例外やRTTIは使用しません。また、動的メモリ割り当ての使用を最小化し、必要に応じてそれを完全に除外する機能を提供します。これは、ベアメタル組み込みシステムの開発時に必要になる場合があります。

Alexは、 C++で通信プロトコルを実装するためのガイド(組み込みシステム向け) というタイトルの優れた無料の電子ブックを提供しています。

1
lorcap

構造体に強く依存します。C++ではstructはデフォルトの可視性publicを持つクラスであることに注意してください。

したがって、これを継承し、これに仮想を追加することもできます。

純粋なデータクラス(C++の用語では標準レイアウトクラス)の場合、これはpackedと組み合わせて機能します。

また、これを開始すると、コンパイラの厳密なエイリアスルールに関する問題が発生する可能性があることに注意してください。これは、メモリのバイト表現(-fno-strict-aliasing あなたの友だちです)。

そうは言っても、それをシリアル化に使用することは強くお勧めします。このためのツール(つまり、protobuf、flatbuffers、msgpack、またはその他)を使用すると、次のような多くの機能が得られます。

  • 言語の独立
  • rpc(リモートプロシージャコール)
  • データ仕様言語
  • スキーマ/検証
  • バージョニング
1
Alex

適切なターゲットOSおよびプラットフォームで使用できるようにするためのニーズに合ったアルゴリズムに向けた擬似コードを次に示します。

C言語を使用する場合、classestemplatesおよびその他のいくつかを使用することはできませんが、_preprocessor directives_を使用して、 OS、アーキテクト_CPU-GPU-Hardware Controller Manufacturer {Intel, AMD, IBM, Apple, etc.}_、_platform x86 - x64 bit_、最後にバイトレイアウトのendianに基づいて必要なstruct(s)。それ以外の場合、ここでの焦点はC++とテンプレートの使用に向けられます。

struct(s)を例に取ります:

_struct Sensor1Telemetry {
    int16_t temperature;
    uint32_t timestamp;
    uint16_t voltageMv;
    // etc...
} __attribute__((__packed__));

struct TelemetryPacket {
    Sensor1Telemetry tele1;
    Sensor2Telemetry tele2;
    // etc...
} __attribute__((__packed__));
_

これらの構造体を次のようにテンプレート化できます。

_enum OS_Type {
    // Flag Bits - Windows First 4bits
    WINDOWS    = 0x01  //  1
    WINDOWS_7  = 0x02  //  2 
    WINDOWS_8  = 0x04, //  4
    WINDOWS_10 = 0x08, //  8

    // Flag Bits - Linux Second 4bits
    LINUX      = 0x10, // 16
    LINUX_vA   = 0x20, // 32
    LINUX_vB   = 0x40, // 64
    LINUX_vC   = 0x80, // 128

    // Flag Bits - Linux Third Byte
    OS         = 0x100, // 256
    OS_vA      = 0x200, // 512
    OS_vB      = 0x400, // 1024
    OS_vC      = 0x800  // 2048

    //....
};

enum ArchitectureType {
    Android = 0x01
    AMD     = 0x02,
    ASUS    = 0x04,
    NVIDIA  = 0x08,
    IBM     = 0x10,
    INTEL   = 0x20,
    MOTOROALA = 0x40,
    //...
};

enum PlatformType {
    X86 = 0x01,
    X64 = 0x02,
    // Legacy - Deprecated Models
    X32 = 0x04,
    X16 = 0x08,
    // ... etc.
};

enum EndianType {
    LITTLE = 0x01,
    BIG    = 0x02,
    MIXED  = 0x04,
    // ....
};

// Struct to hold the target machines properties & attributes: add this to your existing struct.

struct TargetMachine {
    unsigned int os_;
    unsigned int architecture_;
    unsigned char platform_;
    unsigned char endian_;

    TargetMachine() : 
      os_(0), architecture_(0),
      platform_(0), endian_(0) {
    }

    TargetMachine( unsigned int os, unsigned int architecture_, 
                   unsigned char platform_, unsigned char endian_ ) :
      os_(os), architecture_(architecture),
      platform_(platform), endian_(endian) {
    }    
};

template<unsigned int OS, unsigned int Architecture, unsigned char Platform, unsigned char Endian>
struct Sensor1Telemetry {       
    int16_t temperature;
    uint32_t timestamp;
    uint16_t voltageMv;
    // etc...
} __attribute__((__packed__));

template<unsigned int OS, unsigned int Architecture, unsigned char Platform, unsigned char Endian>
struct TelemetryPacket {
    TargetMachine targetMachine { OS, Architecture, Platform, Endian };
    Sensor1Telemetry tele1;
    Sensor2Telemetry tele2;
    // etc...
} __attribute__((__packed__));
_

これらのenum識別子を使用すると、_class template specialization_を使用して、上記の組み合わせに応じてこのclassを必要に応じて設定できます。ここでは、default _class declaration & definition_で正常に動作すると思われるすべての一般的なケースを取り上げ、それをメインクラスの機能として設定します。次に、バイトオーダーの異なるEndian、または異なる方法で何かを行う特定のOSバージョンなどの特殊なケース、または__attribute__((__packed__))#pragma pack()を使用する_GCC versus MS_コンパイラーは、いくつかの特殊化になる可能性がありますそれを説明する必要があります。考えられるすべての組み合わせに特化を指定する必要はありません。これは非常に困難で時間がかかるため、発生する可能性のあるいくつかのまれなケースのシナリオを実行するだけで、ターゲットオーディエンスに対して常に適切なコード指示を確実に行うことができます。 enumsも非常に便利なのは、これらを関数の引数として渡すと、ビットフラグとして設計されているため、一度に複数のものを設定できることです。したがって、このテンプレート構造体を最初の引数として使用し、サポートされているOSを2番目の引数として使用する関数を作成する場合、利用可能なすべてのOSサポートをビットフラグとして渡すことができます。

これにより、この_packed structures_のセットが適切なターゲットに応じて「パック」および/または正しく整列され、異なるプラットフォーム間での移植性を維持するために常に同じ機能を実行できるようになります。

これで、異なるサポートコンパイラのプリプロセッサディレクティブ間でこの特殊化を2回行う必要が生じる場合があります。そのため、現在のコンパイラがGCCである場合、特殊化を使用して構造体を定義し、別のClang、またはMSVC、コードブロックなどで定義します。指定されたシナリオまたはターゲットマシンの属性の組み合わせで適切に使用されていることを強く確認してください。

1
Francis Cugler