RustでX68000のIOCSコールを活用
今回はRustのコードからXCのライブラリを経由してX68000のIOCSコールを呼び出します。
これまでの記事ではwasm2cを使ってRustから生成したwasmを実行形式にする段階でC言語で書かれたグルーコードを通じてC言語向けのライブラリを呼び出していました。今回の記事ではライブラリを直接Rustコードから呼び出す方法を確立していきます。
bindgenを使ったC言語ヘッダファイルの変換
XCのIOCSライブラリをRustのコードで利用するためにヘッダファイルをbindgen[1]で変換していきます。
変換前のヘッダファイルの一部を下記に抜粋します。
/*
* iocslib.h X68k XC Compiler v2.11 Copyright 1990,91,92,93 SHARP/Hudson
*/
#ifndef __IOCSLIB_H
#define __IOCSLIB_H
#include <class.h>
...
struct PATST {
short OFFSETX;
short OFFSETY;
short shadow[16];
short pattern[16];
};
struct READCAP {
unsigned int block;
unsigned int size;
};
struct INQUIRY {
unsigned char unit;
unsigned char info;
unsigned char ver;
unsigned char reserve;
unsigned char size;
unsigned char buff[];
};
#ifdef __PROTO_TYPE
int TRAP15(struct REGS *, struct REGS *);
int B_KEYINP(void);
int B_KEYSNS(void);
int B_SFTSNS(void);
int BITSNS(int);
今回はbindgen CLIを使ってコマンドラインから変換を行います。
iocslib.h
の中で別のヘッダファイルを読み込んでいるのでI
オプションでインクルードのパスを指定しています。
$ bindgen ../../xdev68k/include/xc/iocslib.h \
--no-layout-tests \
--merge-extern-blocks \
--wasm-import-module-name x68iocs \
-o iocs.rs -- -I ../../xdev68k/include/xc
このbindgenコマンドを実行した結果、下記のRustコードが生成されます。
ヘッダで定義されていた構造体やextern宣言がRustのコードに変換されているのがわかります。
プリミティブ型は::std::os::raw
モジュールの型に変換されています。
/* automatically generated by rust-bindgen 0.69.4 */
#[repr(C)]
#[derive(Default)]
pub struct __IncompleteArrayField<T>(::std::marker::PhantomData<T>, [T; 0]);
...
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct PATST {
pub OFFSETX: ::std::os::raw::c_short,
pub OFFSETY: ::std::os::raw::c_short,
pub shadow: [::std::os::raw::c_short; 16usize],
pub pattern: [::std::os::raw::c_short; 16usize],
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct READCAP {
pub block: ::std::os::raw::c_uint,
pub size: ::std::os::raw::c_uint,
}
#[repr(C)]
#[derive(Debug)]
pub struct INQUIRY {
pub unit: ::std::os::raw::c_uchar,
pub info: ::std::os::raw::c_uchar,
pub ver: ::std::os::raw::c_uchar,
pub reserve: ::std::os::raw::c_uchar,
pub size: ::std::os::raw::c_uchar,
pub buff: __IncompleteArrayField<::std::os::raw::c_uchar>,
}
#[link(wasm_import_module = "x68iocs")]
extern "C" {
pub fn TRAP15(arg1: *mut REGS, arg2: *mut REGS) -> ::std::os::raw::c_int;
pub fn B_KEYINP() -> ::std::os::raw::c_int;
pub fn B_KEYSNS() -> ::std::os::raw::c_int;
pub fn B_SFTSNS() -> ::std::os::raw::c_int;
pub fn BITSNS(arg1: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
ライブラリの利用
bindgenで変換したファイルを利用するにはRustのソースコードの先頭で以下のようにincludeします。
#![allow(non_snake_case)]
include!("./iocs.rs");
続く節ではファイルで定義された関数をRustコードから呼び出した時にビルド結果としてどのようなwasmファイルが生成されるのか、さらにはwasm2cで変換をかけたときにどのようなC言語のコードが作られるのか内容を吟味していきたいと思います。
引数も戻り値も無い関数
まずはもっとも単純な引数も戻り値も無い関数を呼び出すコードを見ていきます。
次のRustコードはカーソル位置を移動するIOCSコールを呼び出すだけのプログラムになっています。
#![allow(non_snake_case)]
include!("./iocs.rs");
mod utils;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn main() {
unsafe {
B_DOWN_S();
}
}
このコードをビルドすると下記の短いwasmコードが生成されます。
B_DOWN_S
(0番)をインポートしてmain
(1番)から呼び出している(call 0
)のがわかります。
(module
(type (;0;) (func))
(import "x68iocs" "B_DOWN_S" (func (;0;) (type 0)))
(func (;1;) (type 0)
call 0)
(memory (;0;) 17)
(export "memory" (memory 0))
(export "main" (func 1)))
これをもとに生成されたC言語のコードの主要個所を下記に抜粋します。
B_DOWN_S
はプロトタイプだけが出力されており、f0
関数から利用されています。
void B_DOWN_S(void);
static void f0(void);
...
void wasm_main(void) {
init();
f0();
}
static void f0(void) {
B_DOWN_S();
goto l0;
l0:;
}
これをビルドするとライブラリのB_DOWN_S
がリンクされ正常に実行ファイルが生成されます。
実行してみるとカーソルが1段下に下がることによって1行空けてコマンドプロンプトに戻ってくる表示となります。地味ですが正常に実行されているようです。
プリミティブ型をやり取りする関数
続いてプリミティブ型を引数や戻り値にとる関数を利用してみます。
下記のコードをビルドしていきます。
キー入力があればその内容を確認してカーソルキーの入力であれば方向に応じてLURD
の1文字を出力する処理となっています。
#[wasm_bindgen]
pub fn main() {
loop {
unsafe {
if B_KEYSNS() != 0 {
match B_KEYINP() & 0xFF00 {
0x3b00 => {
B_PUTC('L' as i32);
},
0x3c00 => {
B_PUTC('U' as i32);
},
0x3d00 => {
B_PUTC('R' as i32);
},
0x3e00 => {
B_PUTC('D' as i32);
},
_ => {
}
}
}
}
}
}
これをビルドしていくとC言語のソースは下記のようになります。
プリミティブ型をスタックでやり取りする処理はC言語のローカル変数での受け渡しに変換されるのでエンディアンのことなどを特に心配する必要はなさそうです。
static void f0(void) {
uint32_t l0 = 0;
goto l2;
l2:;
uint32_t l3 = B_KEYSNS();
l3 = !l3;
if (l3) {
goto l2;
}
l3 = B_KEYINP();
uint32_t l4 = UINT32_C(0xFF00);
l3 &= l4;
l4 = UINT32_C(0x3B00);
l3 -= l4;
l0 = l3;
l4 = UINT32_C(0x3FF);
l3 = l3 > l4;
if (l3) {
goto l2;
}
l4 = l0;
l3 = UINT32_C(0x6);
l4 >>= l3 & 0x1F;
l3 = UINT32_C(0xFFF00000);
l4 -= l3;
l3 = load32_align2((const uint32_t *)&m0[l4 + UINT32_C(0)]);
l4 = B_PUTC(l3);
(void)l4;
goto l2;
abort();
goto l1;
l1:;
}
static void init_data(void) {
p0 = UINT32_C(17);
c0 = p0;
m0 = calloc(c0, UINT32_C(1) << 16);
static const uint8_t s0[UINT32_C(13)] = {
0x4C, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x44,
};
memcpy(&m0[UINT32_C(0x100000)], s0, UINT32_C(13));
}
これもそのままビルドが通って、期待通りの動作をしました。
文字列を受け取る関数
続いては文字列などのヒープメモリ上のデータを取り扱う必要があるIOCSコールについてみていきます。
B_PRINT
を使ってHello Worldするコードは下記のようになります。
#![allow(non_snake_case)]
include!("./iocs.rs");
mod utils;
#[link(wasm_import_module = "x68kutil")]
extern "C" {
fn convert_native_ptr_mut(ptr: *mut u8) -> *mut u8;
fn convert_native_ptr(ptr: *const u8) -> *const u8;
}
use wasm_bindgen::prelude::*;
use std::ffi::CString;
#[wasm_bindgen]
pub fn main() {
let c_string = CString::new("Hello IOCS!").unwrap();
unsafe {
B_PRINT(convert_native_ptr_mut(c_string.into_raw() as *mut u8));
}
}
wasmを経由して変換を行う方式をとっているためヒープ上の文字列はwasmのリニアメモリ上に配置されることになります。このためポインタを単純にB_PRINT
に渡してしまうと、wasmリニアメモリ内のアドレスが関数の引数として引き渡されることになってしまいます。これを実メモリ上のアドレスに変換するためにconvert_native_ptr_mut
関数を導入しました。
convert_native_ptr_mut
の実装は下記のようになります。wasmリニアメモリの内容を格納している配列m0
上の指定インデックスのポインタを返す処理になっています。この関数はwasm2cが自動的にC言語のソースコードに埋め込むようにしました。[2]
static inline uint32_t convert_native_ptr_mut(uint32_t adr) {
return (uint32_t)&m0[adr];
}
これもビルドしてHelloメッセージが表示できることを確認できました。
構造体を受け取る関数
最後に構造体を引数として取る関数を見ていきます。
構造体はwasmのリニアメモリ上に配置されるときに16bit、32bitのプリミティブ型がリトルエンディアンにされてしまいます。このままだと意図しない値の入った構造体がIOCSコールに渡されることになるため構造体内にビッグエンディアンで値を格納するためのトリックを導入する必要があります。
下記にIOCSコールを使ってグラフィック(FILL)を描画するコードを示します。
#![allow(non_snake_case)]
include!("./iocs.rs");
mod utils;
#[allow(dead_code)]
#[link(wasm_import_module = "x68kutil")]
extern "C" {
fn convert_native_ptr_mut(ptr: *mut u8) -> *mut u8;
fn convert_native_ptr(ptr: *const u8) -> *const u8;
fn get_native_i16(ptr: *const i16) -> i16;
fn set_native_i16(ptr: *mut i16, value: i16);
fn get_native_u16(ptr: *const u16) -> u16;
fn set_native_u16(ptr: *mut u16, value: u16);
fn get_native_i32(ptr: *const i32) -> i32;
fn set_native_i32(ptr: *mut i32, value: i32);
fn get_native_u32(ptr: *const u32) -> u32;
fn set_native_u32(ptr: *mut u32, value: u32);
}
trait NativePtr {
fn as_native_ptr(&self) -> *const Self;
fn as_native_mut_ptr(&mut self) -> *mut Self;
}
impl<T> NativePtr for T {
fn as_native_ptr(&self) -> *const T {
unsafe { convert_native_ptr((self as *const T) as *const u8) as *const T }
}
fn as_native_mut_ptr(&mut self) -> *mut T {
unsafe { convert_native_ptr_mut((self as *mut T) as *mut u8) as *mut T }
}
}
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn main() {
unsafe {
CRTMOD(14); // 256x256 16BITx1 RES_HIGH
G_CLR_ON();
let mut fillptr = FILLPTR {
x1: 0,
y1: 0,
x2: 0,
y2: 0,
color:0xFFFF
};
set_native_i16(&mut fillptr.x2, 16);
set_native_i16(&mut fillptr.y2, 16);
FILL(fillptr.as_native_mut_ptr());
}
}
FILL
はFILLPTR
構造体を引数にとるため、この構造体を仮の値で生成したのちにset_native_i16
関数でビッグエンディアンで値を書き込んでいます。この関数の実装は下記のようになります。この関数もwasm2c
で自動生成するようにしました。
static inline void set_native_i16(uint32_t adr, int16_t value) {
*(int16_t *)&m0[adr] = value;
}
ややすっきりしないコードとなってしまいますが、ビルドして実行すると無事16×16ドットのボックスが描画されました。
なお、IOCSコールから戻った後も構造体をメモリ上に保持する必要がある場合にはRustのメモリライフサイクルに関する動作や、wasmのリニアメモリがmemory.growで配置変更になる可能性を考慮する必要があるためもう少し複雑な処理を行う必要が出てきます。このような処理に関しては今回の記事では取り扱いません。
まとめ
今回はXCのライブラリを使ってIOCSコールをRustコードから呼び出すことに取り組みました。
この記事の方法で他のC言語向けライブラリの呼び出しも可能になると思います。
次回予告
次回はヒープ、スタック領域の削減に取り組みたいと思います。
Discussion