【Rust】生ポインタとその演算(戻せるスライスの実装)

2020-04-02

Rustでスライスを扱う場合前に進めることはできても、後ろに戻すことができない。 C言語のプログラムから移植するなどでそのような操作をしたい場合にどうしたらいいか調べてみた。

モチベーション

C言語で組まれたプログラムをRustで実装する際に、配列の途中を指すようなポインタはスライスで扱うことになると思う。 スライスはC言語でいうところのポインタに加えて長さを保持しているもので(ファットポインタ)、それによって範囲外アクセスが起きないようチェックできる。

ポインタを前に進める場合に対応するには、スライスをさらにスライス取ればいい:

// C言語でいう ptr += 1;
let slice = &slice[1..];

でポインタを戻す場合にはどうしたらいいか。 スライスでは後ろに戻した場合の安全性を確認できないため、そのような操作ができない。

ということで、生ポインタの演算に手を出してみた。

生ポインタの操作

生ポインタの取得

生ポインタを取得するには、参照から as *const T (イミュータブルの場合)または as *mut T (ミュータブルの場合)でキャストしてやる:

let array: [i32; 6] = [11, 22, 33, 44, 55, 66];
let slice: &[i32] = &array;
// C: int32_t *rawptr = &slice[0];
let rawptr: *const i32 = &slice[0] as *const i32; // <= 生ポインタ取得
println!("*{:?} = {}", rawptr, unsafe{*rawptr}); // <= *0x7ffee8de5020 = 11

スライスの as_ptr メソッドでも取得できる。

生ポインタへの整数加減算

生ポインタを進めたり戻したりするには offset メソッドを使用する:

// C: int32_t *rawptr2 = rawptr + 4;
let rawptr2: *const i32 = unsafe { rawptr.offset(4) };
println!("*{:?} = {}", rawptr2, unsafe{*rawptr2}); // <= *0x7ffee8de5030 = 55

// C: int32_t *rawptr3 = rawptr2 - 2;
let rawptr3: *const i32 = rawptr2.wrapping_offset(-2);
println!("*{:?} = {}", rawptr3, unsafe{*rawptr3}); // <= *0x7ffee8de5028 = 33

C言語と同様に、整数を加減算した場合にはその値そのものじゃなく、要素サイズ倍した値がポインタ値に加算される。

wrapping_offset というメソッドもあって、こちらは unsafe が必要ない。 説明を読むと offset メソッドのほうが適切に最適化される、と書いてあるがどういうことなんだろうか? ラッピング演算でポインタが境界内でなくてもよい、ということなので感覚的には逆だと思ってしまうんだが…。

addsub というのもあるが、内部では単に offset を呼んでいるだけ。

ポインタ間の距離

ポインタ間の距離(要素数)を計算するには offset_from をメソッド使用する:

#![feature(ptr_offset_from)]

// C: size_t ofs = rawptr3 - slice;
let ofs: isize = unsafe { rawptr3.offset_from(slice.as_ptr()) };
println!("{}", ofs); // <= 2

こちらもC言語同様に、要素サイズで割った値になる。 experimentalとのことで、 #![feature(ptr_offset_from)] の指定が必要。

同様に wrapping_offset_from メソッドがある。

スライスに戻す

生ポインタからスライスに変換するには、 slice::from_raw_parts を使用する:

let restored: &[i32] = unsafe { std::slice::from_raw_parts(rawptr3, slice.len() - ofs as usize) };
println!("#{}: {:?}", restored.len(), restored); // <= #4: [33, 44, 55, 66]

モジュール化する

生ポインタをゴリゴリ触って unsafe をそこら中に書き散らして未定義動作の発生を拡散させてしまっては、なんのためにRustで書いてんの?ということになってしまう。 なのでいっちょモジュール化してみる。

#![feature(ptr_offset_from)]

use std::ops::Index;

pub struct RewindableSlice<'a, T> {
slice: &'a [T],
behind_count: usize,
}

impl<'a, T> RewindableSlice<'a, T> {
pub fn new(slice: &'a [T]) -> Self {
Self {
slice,
behind_count: 0,
}
}

pub fn to_slice(&self) -> &'a [T] {
self.slice
}

pub fn forward(&mut self, d: usize) {
self.slice = &self.slice[d..];
self.behind_count += d as usize;
}

pub fn backward(&mut self, d: usize) {
if d > self.behind_count {
panic!(
"index out of bounds: the slice can moves backwards only {}, but try to move {}",
self.behind_count, d
);
}

let rawptr = self.slice.as_ptr();
let rawptr = unsafe { rawptr.offset((d as isize).wrapping_neg()) };
self.slice = unsafe { std::slice::from_raw_parts(rawptr, self.slice.len() + d) };
self.behind_count -= d;
}
}

impl<'a, T> Index<usize> for RewindableSlice<'a, T> {
type Output = T;
fn index(&self, i: usize) -> &Self::Output {
&self.slice[i]
}
}

forward で進める際に進めた数をカウントしておく。 backward で戻る際に範囲外になってしまう場合には panic! を起こすようにする。

使用は:

fn main() {
let array: [i32; 6] = [11, 22, 33, 44, 55, 66];

let mut rs = RewindableSlice::new(&array);
rs.forward(4);
println!("{:?}", rs.to_slice());

//slice.forward(3); // <= panic!

rs.backward(3);
println!("{}", rs[1]);

//slice.backward(2); // <= panic!
}