Rustでプライベートフィールドにアクセスする荒技
この記事は Rust Advent Calendar 2023 シリーズ2 14日目 の記事です。
Rustでアプリケーション開発をしていると、稀にそのフィールドはパブリックにして欲しかったというフィールドのあるライブラリがあります。私はありました。
そういったときに使える荒技として備忘録がてら残そうと思います。
プライベートフィールドを持つ構造体の例
今回の例として使うサンプル実装は下記になります。
mod foo {
pub struct PrivateField {
v1: i32,
v2: i32,
}
}
fn main() {
let f = foo::PrivateField { v1: 1, v2: 2 };
println!("v1: {}, v2: {}", f.v1, f.v2);
}
このコードをビルドするといくつかエラーが発生します。
error[E0616]: field `v1` of struct `PrivateField` is private
--> src/foo.rs:10:34
|
10 | println!("v1: {}, v2: {}", f.v1, f.v2);
| ^^ private field
error[E0616]: field `v2` of struct `PrivateField` is private
--> src/foo.rs:10:40
|
10 | println!("v1: {}, v2: {}", f.v1, f.v2);
| ^^ private field
フィールドがパブリックではないのでアクセスできないというエラーですね。
ここからこのPrivateFiled
を外部ライブラリのモジュールとみなして変更せずにアクセスできるようにする方法を考えましょう。
ポインタを使ってプライベートフィールドを参照する
Rustではリフレクションはないため、Javaのようにリフレクションを使ってプライベートフィールドにアクセスすることはできません。
しかし、メモリ上に存在しているため、ポインタを駆使すればその値を取得することができます。
- The fields are properly aligned.
- The fields do not overlap.
- The alignment of the type is at least the maximum alignment of its fields.
Rustの構造体のデフォルトレイアウトは上記の説明の形になります。
つまり、今回の例では
- 0: 4 bytes
- 4: 4 bytes
というメモリレイアウトになるということですね。
ポインタの開始位置は変数から取得できるので、これでプライベートフィールドにアクセスができます。
fn main() {
// create struct
let data = &[1i32, 2i32];
let ptr1 = data.as_ptr() as *const foo::PrivateField;
let f = unsafe { ptr1.read() }; // readでメモリコピーが発生する
// read private field
let ptr2 = &f as *const foo::PrivateField;
let v1_ptr = ptr2 as *const i32;
unsafe {
let v1 = *v1_ptr;
let v2 = *v1_ptr.add(1); // size_of::<T> * 1ポインタを移動
println!("v1: {}, v2: {}", v1, v2);
}
}
v1: 1, v2: 2
無事にコンパイルが通り、プライベートフィールドの値を参照することができました。
構造体のコピーを作成する
1つか、2つぐらいのフィールドにアクセスするならポインタでも構いませんが、この数が増えるとポインタで1つずつ取り出すのは冗長です。
そこで、同一の構造を持つ構造体を作成してアクセスを容易にします。
struct PublicField {
v1: i32,
v2: i32,
}
fn main() {
// create struct
let ptr = (&PublicField {
v1: 1,
v2: 2,
}) as *const PublicField as *const foo::PrivateField;
let f = unsafe { ptr.read() }; // readでメモリコピーが発生する
// read private field
unsafe {
let ptr = &f as *const foo::PrivateField as *const PublicField;
let f = ptr.read(); // readでメモリコピーが発生する
println!("v1: {}, v2: {}", f.v1, f.v2);
}
}
v1: 1, v2: 2
この方法でもプライベートフィールドにアクセスすることができました。
共用体を使ってプライベートフィールドを参照
先ほどまでのポインタを使った方法では値を取得しようとするとread
でメモリコピーが発生します。(&
で参照するならread
は不要なのでコピーしないこともできます)
しかし、値を取得して、かつメモリコピーを行わないようにするには別の方法を使います。
共用体を使うことで各フィールドが同一のメモリアドレスを参照することができるため、メモリコピーをせずに値を参照することができます。
union Field {
a: ManuallyDrop<foo::PrivateField>,
b: ManuallyDrop<PublicField>,
}
fn main() {
// create struct
let data = &[1i32, 2i32];
let ptr = data.as_ptr() as *const foo::PrivateField;
let f = unsafe { ptr.read() };
let f = Field { a: ManuallyDrop::new(f) };
// read private field
unsafe {
println!("v1: {}, v2: {}", f.b.v1, f.b.v2);
println!("a ptr: {:p}, b ptr: {:p}", &f.a, &f.b);
}
}
v1: 1, v2: 2
a ptr: 0x16d7aa568, b ptr: 0x16d7aa568
これでもプライベートフィールドを参照することができました。
ポインタを使った方法と比べて幾分かスッキリとした書き方になるため、プライベートフィールドにアクセスしたい場合は同一の構造体を用意して共用体を使うのが個人的に好みです。
おわりに
いろいろと説明しましたが、プロダクションコードでこの荒技は使うべきではありません。
特にライブラリが公開している構造体のプライベートフィールドにアクセスしたいという理由で使うべきではありません。素直にプルリクエストを送ってライブラリ側を変更しましょう。
Discussion