kubo39's blog

ただの雑記です。

Is it safe in safe cast in safe functions?

safe functionの定義 より、

The following operations are not allowed in safe functions: * No casting from a pointer type to any type with pointers other than void*.

void*を除く異なるポインタ型へのキャストは@safe関数内では禁止されている、という文言にみえる。

実際に例をあげると、範囲外となるようなメモリ領域への参照を含んでしまうようなポインタの変換操作は禁止されている。

// これはコンパイルエラー
void foo() @safe
{
    int* p = new int(42);
    long* lp = cast(long*) p;  // Error: cast from `int*` to `long*` not allowed in safe code
}

しかし現時点(dmd 2.100.0)では float -> int のキャストのような操作はコンパイルが通ってしまう。 intとfloatはバイトサイズこそ同じだが、整数の2の補数表現とIEEE754の浮動小数点数の各ビットの意味合いは全く異なるので、ほとんどのユーザが意図しないような結果になってしまう。

void foo() @safe
{
    float* fp = new float(1.0);
    int* p = cast(int*) fp;
    writefln("float: %f", *fp);  // --> float: 1.000000
    writefln("int: %d", *p);     // --> int: 1065353216
}

こういった類の変換操作が有用となるケースはあるが、そういった場合にこそ@trusted関数を使うべきではないかと感じる。

また ulong -> uint のようなメモリ領域上は範囲外とならないようなキャストは行えてしまう。 このコードは結果がホスト環境のエンディアンによって変わってしまう。

void foo() @safe
{
    ulong* lp = new ulong(1UL << 32);
    uint* p = cast(uint*) lp;
    writefln("uint: %d", *p);    // --> uint: 0 (ホストがリトルエンディアンの時)
    writefln("ulong: %d", *lp);  // --> ulong: 4294967296
}

これは仕様と実装が乖離しているようにみえる。 仕様と実装のどちらかが間違っているということになるが、実際どちらが正しいだろうか。

仕様が正しいとすると実装側が間違っているということで、これは実装側を修正するだけなので単純なケースとなる。 ただし厳密にAny type with pointersを守る場合、int -> intのような無意味ではあるが問題のないポインタのキャスト操作まで禁止すべきかという話がある。

実装側が正しい場合、つまりこれらの処理を@safe関数内で行っても問題ないという立場の場合はsafe functionsの仕様を修正する必要があるが、その場合はどういった操作を禁止するべきなのか決める必要がある。

最初に書いたメモリの範囲外アクセスが発生しうるようなキャストは禁止しているためこれは明示的な記述を行うようになるだろうが、他にどのような操作は禁止すべきなのかもれなくリストアップするためには調査が必要となるだろう。