Rust FFIでCユニオンを使用する

Herman J. Radtke IIIによる記事「 Rust FFIでのCユニオンの操作 」の翻訳に注目してください。



注:この記事では、読者がRust FFIバイト順 (エンディアン)、およびioctlに精通していることを前提としています



Cコードのバインダーを作成するとき、ユニオンを含む構造に遭遇することは避けられません。 Rustには結合のサポートが組み込まれていないため、独自の戦略を立てる必要があります。 Cでは、ユニオンは同じメモリ領域に異なるタイプのデータを格納するタイプです。 整数と浮動小数点数のバイナリ表現間の変換、擬似多態性の実装、ビットへの直接アクセスなど、結合が好ましい理由は多数あります。 擬似多態性に焦点を当てます。



例として、インターフェイス名に基づいてMACアドレスを取得しましょう。 取得に必要な手順をリストします。





MACアドレスの取得に関する詳細に興味がある場合は、この手順を参照してください



C ioctlで宣言された関数を使用し、そこでifreq構造体を渡す必要があります。 /usr/include/net/if.hを見ると、ifreqが次のように定義されていることがわかります。



struct ifreq { char ifr_name[IFNAMSIZ]; union { struct sockaddr ifru_addr; struct sockaddr ifru_dstaddr; struct sockaddr ifru_broadaddr; short ifru_flags; int ifru_metric; int ifru_mtu; int ifru_phys; int ifru_media; int ifru_intval; caddr_t ifru_data; struct ifdevmtu ifru_devmtu; struct ifkpi ifru_kpi; u_int32_t ifru_wake_flags; u_int32_t ifru_route_refcnt; int ifru_cap[2]; } ifr_ifru; }
      
      





連合ifr_ifruには問題が発生します。 ifr_ifruで可能なタイプを見ると、すべてが同じサイズではないことがわかります。 shortは2バイトで、u_int32_tは4バイトです。 サイズが不明ないくつかの構造は、状況をさらに複雑にします。 Rustで正しいコードを作成するには、ifreq構造体の正確なサイズを見つけることが重要です。 小さなCプログラムを作成しましたが、ifreqはifr_nameに16バイトを使用し、ifr_ifruに24バイトを使用していることがわかりました。



構造の正確なサイズに関する知識を身に付けて、Rustで表現を開始できます。 1つの戦略は、すべてのタイプの関連付けに特化した構造を作成することです。



 #[repr(C)] pub struct IfReqShort { ifr_name: [c_char; 16], ifru_flags: c_short, }
      
      





IfReqShortを使用して、SIOCGIFINDEXを照会できます。 この構造は、Cのifreq構造よりも小さいです。2バイトしか書き込まれないと想定していますが、外部ioctlインターフェイスは24バイトを想定しています。 セキュリティのために、最後に22バイトのパディングを追加しましょう。



 #[repr(C)] pub struct IfReqShort { ifr_name: [c_char; 16], ifru_flags: c_short, _padding: [u8; 22], }
      
      





次に、ユニオンの各タイプに対してこのプロセスを繰り返す必要があります。 多くの構造を作成し、サイズを間違えないように非常に注意する必要があるため、これはやや面倒です。 ユニオンを表す別の方法は、生のバイトバッファを使用することです。 次のように、Rustのifreq構造の単一の表現を作成できます。



 #[repr(C)] pub struct IfReq { ifr_name: [c_char; 16], union: [u8; 24], }
      
      





このユニオンバッファには、あらゆるタイプのバイトを格納できます。 これで、生のバイトを目的の型に変換するメソッドを定義できます。 トランスミュートの使用を拒否することにより、安全でないコードの使用を避けます。 rawバイトをsockaddr Cタイプに変換して、MACアドレスを取得するメソッドを作成しましょう。



 impl IfReq { pub fn ifr_hwaddr(&self) -> sockaddr { let mut s = sockaddr { sa_family: u16::from_be((self.data[0] as u16) << 8 | (self.data[1] as u16)), sa_data: [0; 14], }; // basically a memcpy for (i, b) in self.data[2..16].iter().enumerate() { s.sa_data[i] = *b as i8; } s } }
      
      





このアプローチにより、未加工バイトを目的のタイプに変換するための1つの構造と方法が残ります。 ifr_ifruユニオンをもう一度見ると、生のバイトからsockaddrを作成する必要がある要求が少なくとも2つあることがわかります。 DRYの原則を使用して、未加工のバイトをsockaddrに変換するプライベートIfReqメソッドを実装できます。 ただし、sockaddr、short、intなどの作成の詳細を抽象化することで、より良い結果を得ることができます。 IfReqから。 必要なのは、特定の型が必要であることを関連付けに伝えることだけです。 これのためにIfReqUnionを作成しましょう:



 #[repr(C)] struct IfReqUnion { data: [u8; 24], } impl IfReqUnion { fn as_sockaddr(&self) -> sockaddr { let mut s = sockaddr { sa_family: u16::from_be((self.data[0] as u16) << 8 | (self.data[1] as u16)), sa_data: [0; 14], }; // basically a memcpy for (i, b) in self.data[2..16].iter().enumerate() { s.sa_data[i] = *b as i8; } s } fn as_int(&self) -> c_int { c_int::from_be((self.data[0] as c_int) << 24 | (self.data[1] as c_int) << 16 | (self.data[2] as c_int) << 8 | (self.data[3] as c_int)) } fn as_short(&self) -> c_short { c_short::from_be((self.data[0] as c_short) << 8 | (self.data[1] as c_short)) } }
      
      





ユニオンを構成する各タイプのメソッドを実装しました。 変換がIfReqUnionによって制御されるようになったので、次のようにIfReqメソッドを実装できます。



 #[repr(C)] pub struct IfReq { ifr_name: [c_char; IFNAMESIZE], union: IfReqUnion, } impl IfReq { pub fn ifr_hwaddr(&self) -> sockaddr { self.union.as_sockaddr() } pub fn ifr_dstaddr(&self) -> sockaddr { self.union.as_sockaddr() } pub fn ifr_broadaddr(&self) -> sockaddr { self.union.as_sockaddr() } pub fn ifr_ifindex(&self) -> c_int { self.union.as_int() } pub fn ifr_media(&self) -> c_int { self.union.as_int() } pub fn ifr_flags(&self) -> c_short { self.union.as_short() } }
      
      





その結果、2つの構造があります。 まず、Cのifreqメモリ構造を表すIfReq。その中に、ioctl要求の各タイプのメソッドを実装します。 次に、Ifrequnionがあります。これは、さまざまなタイプのifr_ifruユニオンを管理します。 必要なタイプごとにメソッドを作成します。 これは、各タイプの共用体に特化した構造を作成するよりも時間がかかりません。また、IfReq自体のタイプ変換よりも優れたインターフェースを提供します。



より完全な既製の例を次に示します。 まだやるべきことがいくつかありますが、テストはパスし、上記の概念がコードに実装されています。



注意してください、このアプローチは理想的ではありません。 ifreqの場合、ifr_nameには16バイトが含まれており、ワード境界で整列されていることが幸運です。 ifr_nameが4バイトのワード境界に揃えられていないと、問題が発生します。 協会のタイプ[u8; 24]、1バイトの境界に配置されます。 24バイトのタイプは、異なるアライメントになります。 問題を説明する短い例を次に示します。 次のユニオンを含むC構造体があるとします。



 struct foo { short x; union { int; } y; }
      
      





この構造のサイズは8バイトです。 xには2バイト、アライメントにはさらに2バイト、yには4バイト。 これをRustで描写してみましょう。



 #[repr(C)] pub struct Foo { x: u16, y: [u8; 4], }
      
      





Foo構造のサイズは6バイトのみです。xの場合は2バイト、xと同じ4バイトのワードに配置された最初の2つのu8要素。 この微妙な違いは、8バイトサイズの構造を想定しているC関数に渡すときに問題を引き起こす可能性があります。



Rustが結合をサポートするまで、このような問題を正しく解決することは困難です。 頑張ってください、しかし注意してください!



All Articles