なんか考えてることとか

変な人が主にプログラミング関連で考えていることをまとめる。

JavaScriptでC共用体を表現してみる(実は、リベンジ)。

  • 2022/8/5
    • 「参照渡し」の誤用があったのですべて修正

その記事は(ブログごと)消していてもうないのだが、実は筆者は過去、JavaScript(以下JSと呼ぶ)でCの共用体について説明しよう、という記事を書こうとしたことがある。しかし、複数の変数がメモリを共用する、というのをコーディングする際あまりメモリを気にする必要のないJSでは表現することは難しくそのときは「残念ながら表現することは不可能だ」という結論となっていた。そのため、配列などを利用すると似たようなことができる、程度のことしか書いていなかった。

しかし近年、JSの知識を深めていて「あ、それっぽいことは実現できそうだぞ」と思ったので、ここにその手法を記す。

環境

JSの環境はECMAScript 2015以降、C共用体の説明時に使うCの環境はC11以降とする。

そもそもC共用体って何?

Cにおける共用体とは、複合型*1の一種であり、Cでは構造体のように定義する。

#include <stdint.h>

/**
 * C共用体
 */
typedef union  {
    int8_t i8[4];
    int32_t i32;
} bytes4_t;

構造体との違いは「メモリを共用する」という点である。構造体はそれぞれのメンバ変数が異なるメモリを持っているが共用体はそれぞれのメンバ変数が同じメモリを共有している。簡単なイメージとしては、以下のような感じである。

そのため「一つのメンバが変化するとほかのメンバも変化する」という性質がある。

#include <stdint.h>
#include <stdio.h>

/**
 * C共用体
 */
typedef union  {
    int8_t i8[4];
    int32_t i32;
} bytes4_t;

int main(void)
{
    int i = 0;
    
    /* 変数の初期化 */
    bytes4_t b = { .i8 = { 0 } };
    
    /* メンバ変数i32の変更 */
    b.i32 = 0x1234;
    
    /* 共用体であるため、メンバ変数i8も変わっているはず */
    printf("b\n");
    for (i = 0; i < 4; ++i) {
        printf(" .i8[%d]: 0x%02x\n", i, b.i8[i]);
    }
}

b
 .i8[0]: 0x00
 .i8[1]: 0x00
 .i8[2]: 0x12
 .i8[3]: 0x34

b
 .i8[0]: 0x34
 .i8[1]: 0x12
 .i8[2]: 0x00
 .i8[3]: 0x00

変更したのはb.i32であるはずなのに、b.i8にも変更が起きていることがわかるだろうか。これこそメモリを共有しているからこのようになるのである。

では、JSではこのような仕組みは実現可能であろうか。結論から言うと「シャローコピー*2」という仕組みを利用すれば、不可能と言うわけでもない。

// Array型の変数aを宣言
const a = [0];

// Array型の変数bを宣言
// この時点でシャローコピーしている
const b = a;

// b[0]を変更
b[0] = 42;

// a[0]は42となっているはず
console.log(`a[0]: ${a[0]}`);

a[0]: 42

ただこのシャローコピーは「同じ型の値としてでしか読むことができない」という性質があるため、これだけでC共用体のような仕組みを作ることはできない。

筆者の思うC共用体とは「違う型の値として読むこともできる」ようなデータ構造である。しかし先ほどご覧になった通りシャローコピーだけではそれは実現できないようである。
そこで筆者が思いついたのは以下のようなデータ構造である。

元となるプロパティを用意して複数のgetter/setterを用意する

getter/setterとは

ECMAScript5以降のJSにもgetter/setterの概念があり*3、メソッドをあたかも普通のプロパティのように扱うことができる。
JSでgetter/setterを使った例を、以下に示す。

/**
 * 長方形クラス
 * @note privateプロパティを実現するためのイディオムはあるが、わかりやすさの
 *       ために`_`が先頭に付くものをprivateプロパティとする
 */
class Rectangle {
    /**
     * コンストラクタ
     */
    constructor(width, height) {
        this._width  = width;   // 幅
        this._height = height;  // 高さ
    }
    
    /**
     * 面積を取得するgetter
     */
    get area() {
        return this._width * this._height;
    }
    
    /**
     * 面積を設定するsetter
     */
    set area([width, height]) {
        this._width  = width;   // 幅
        this._height = height;  // 高さ
    }
}

// 長方形のインスタンスを宣言
const rect = new Rectangle(0, 0);

// 面積を変更する
rect.area = [7, 5]

// 面積を取得する
console.log(`rect.area: ${rect.area}`);

rect.area: 35

つまりこれをベースに、もととなるプロパティを用意して、複数のgettersetterを用意してあげれば、JSでもC共用体のようなデータ構造を作れそうである。

JavaScriptでC共用体のようなデータ構造を作ってみる

では早速作ってみよう。

まずベースとなるプロパティを用意する。ベースとなるプロパティはNumber*4で、それぞれ32bitの符号付き整数型*5*6(以下int32型とする)、String型(ただし4文字で1文字の文字コードは0~255までとする)の値として扱うためのgetter/setterを用意する。

以上に従い作ったクラスは以下のとおりである。

/**
 * int32型、String型の値として読むことのできる共用体
 */
class Bytes4 {
    /**
     * コンストラクタ
     */
    constructor({i32, s}) {
        // 使う側からはthis._baseは見えないようにする
        // enumerableをfalseにすることで使う側からはthis._baseが見えない
        Object.defineProperty(this, "_base", {
            writable:    true,
            enumerable: false
        });
        
        // 初期化の優先順位はthis.s, this.i32, this._base
        switch (true) {
            case typeof s === "string":
                this.s = s;
                
                break;
            case typeof i32 === "number":
                this.i32 = i32;
                
                break;
            default:
                this._base = 0;
        }
    }
    
    /**
     * int32型の値として読むgetter
     */
    get i32() {
        return 0 | this._base;
    }
    /**
     * int32型の値として設定するsetter
     * @note 0x80000000以上のNumber型の値はオーバーフローとなり
     *       負の値となる
     */
    set i32(num) {
        this._base = 0 | num;
    }
    
    /**
     * String型の値として読むgetter
     * @note リトルエンディアンとする
     */
    get s() {
        const i32 = this.i32;
        
        return String.fromCharCode(
            ...[...Array(4)].map((_, index) => {
                const shift = 8 * index;
                return ((0xff << shift) & i32) >>> shift;
            })
        );
    }
    /**
     * String型の値として設定するsetter
     * @note リトルエンディアンとし、各文字コードはラップアラウンドする
     */
    set s(str) {
        this._base = [...str.slice(0, 4)].reduce((acc, c, index) => {
            return acc | ((0xff & c.charCodeAt(0)) << (8 * index));
        }, 0);
    }
}

実際に使ってみる。最初はメンバ変数i320x1234を格納し、メンバ変数s"\x34\x12\x00\x00"に変わっているかどうかを確認する。

// Bytes4のインスタンスを生成
const b = new Bytes4({});

// メンバ変数i32に0x1234を格納
b.i32 = 0x1234;

// メンバ変数sが"\x34\x12\x00\x00"であれば、trueと表示される
console.log(b.s === "\x34\x12\x00\x00");

true

次はその逆にメンバ変数s"\x78\x56"を格納し、メンバ変数i320x5678に変わっているかどうかを確認する。

// Bytes4のインスタンスを生成
const b = new Bytes4({});

// メンバ変数sに"\x78\x56"を格納
b.s = "\x78\x56";

// メンバ変数i32は0x5678となるはず
console.log(`0x${b.i32.toString(16)}`);

0x5678

以上から、JSでC共用体のようなデータ構造が作れたと言える。退避するための一時メモリはあるものの、あたかもi32が変わればsも変わり、sが変わればi32も変わるように見せるよう実装することができた。

注意点というか

確かに、JSでC共用体のようなデータ構造は実装できた。しかし、あくまでそのような実装ができたというだけで「これを用いてJSでC共用体を説明できるか?」と問われれば、答えは間違いなくNoである。
したがって、JSでC共用体を説明することは不可能であると言える。C共用体についてちゃんと説明する場合はCで説明したほうが良いし、そちらのほうが遥かに楽だろう。

*1:簡単に説明すると、複数の単純型(intやfloatなど)で構成された型のことである。共用体のほかに配列や構造体、Simulaベースのオブジェクト指向型言語においてはクラスも複合型であると言える

*2:値をコピーするのではなく「値の参照」をコピーするセマンティクスのこと。詳しくはこちらを参照

*3:ちなみにgetter/setterのある仕組みのことをプロパティと呼ぶが、JavaScriptにおけるプロパティとは違う。JavaScriptにおけるプロパティは、プロトタイプベースにおける「スロット」と同様の概念である

*4:JavaScriptにおけるNumber型は、64bitの浮動小数型と等価である

*5:JavaScriptではそのような型はないように思えるが、実はビット演算などの結果は32bitの符号付き整数型をNumber型に変換した値である(たとえば&演算子の解説を参照)

*6:ただし取得する際はNumber型の値として取得する