From a5662a2516eaa54de976f2ad06797513dd74bd25 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 08:39:33 +0000 Subject: [PATCH 1/5] =?UTF-8?q?feat(pkcl):=20POSIX=20=E4=BA=92=E6=8F=9B?= =?UTF-8?q?=E5=B1=A4=20pkcl=20=E3=81=AE=E8=A8=AD=E8=A8=88=E3=81=A8?= =?UTF-8?q?=E6=9C=80=E5=B0=8F=E5=AE=9F=E8=A3=85=20(#165)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FqgVXKR79fR8rybAwBZ2fY --- doc/spec_pkcl.md | 282 +++++++++++++++++++++++++ foundation/include/pkcl/errno.h | 46 ++++ foundation/include/pkcl/fcntl.h | 29 +++ foundation/include/pkcl/pkcl.h | 33 +++ foundation/include/pkcl/sys/stat.h | 52 +++++ foundation/include/pkcl/sys/types.h | 19 ++ foundation/include/pkcl/unistd.h | 44 ++++ foundation/lib/pkcl/crt.c | 24 +++ foundation/lib/pkcl/errno.c | 68 ++++++ foundation/lib/pkcl/fd.c | 316 ++++++++++++++++++++++++++++ foundation/lib/pkcl/fs.c | 138 ++++++++++++ foundation/tests/pkcl/guest.c | 109 ++++++++++ foundation/tests/pkcl/test_pkcl.py | 68 ++++++ memory/2026-10-25-issue-165-pkcl.md | 11 + 14 files changed, 1239 insertions(+) create mode 100644 doc/spec_pkcl.md create mode 100644 foundation/include/pkcl/errno.h create mode 100644 foundation/include/pkcl/fcntl.h create mode 100644 foundation/include/pkcl/pkcl.h create mode 100644 foundation/include/pkcl/sys/stat.h create mode 100644 foundation/include/pkcl/sys/types.h create mode 100644 foundation/include/pkcl/unistd.h create mode 100644 foundation/lib/pkcl/crt.c create mode 100644 foundation/lib/pkcl/errno.c create mode 100644 foundation/lib/pkcl/fd.c create mode 100644 foundation/lib/pkcl/fs.c create mode 100644 foundation/tests/pkcl/guest.c create mode 100644 foundation/tests/pkcl/test_pkcl.py create mode 100644 memory/2026-10-25-issue-165-pkcl.md diff --git a/doc/spec_pkcl.md b/doc/spec_pkcl.md new file mode 100644 index 0000000..3952260 --- /dev/null +++ b/doc/spec_pkcl.md @@ -0,0 +1,282 @@ +# pkcl 仕様(POSIX Kernel Compatibility Layer)v0.1 + +pkcl(発音は paxel)は、POSIX の関数呼び出しを Foundation の IPC・libfnd 呼び出しへ**コンパイル時にリンクして**変換する層です。Fuchsia の fdio に相当します。POSIX のコード(最終目標は BusyBox)を、カーネルや fs を POSIX 化せずに Foundation 上で動かすために置きます。 + +状態:v0.1 は設計と最小実装(M0、§10)です。この文書の「実装済み」と書いた部分だけが動きます。 + +## 1. 原則 + +1. **カーネルが本質的に持つ機能だけを写す**。Foundation のカーネルは `fork`、シグナル、パイプ、UID、シンボリックリンクを持ちません。それらは pkcl が偽装した「動くふり」にせず、§6 の表のとおり、**エミュレーションできるもの**と**エラー(`ENOSYS` など)にするもの**に分けます。 +2. **少しの変更で実装できるものはエミュレーションする**。ただし新しい仕組みを横に足さず、Foundation 側の契約(`spec_Foundation.md`、`spec_Foundation_API.md`)に沿うこと。契約を変える必要があるものは §7 のギャップとして Issue にし、pkcl はその間 `ENOSYS` を返します。 +3. **コンパイル時の層**。pkcl は静的ライブラリ(ソース)とヘッダで、アプリケーションに直接リンクします。サービスやプロセスを追加しません。カーネルの ABI は変えません。 +4. **libfnd の上に積む**。pkcl は Foundation を ``(WAL `fnd`)経由でだけ使い、syscall を直接呼ぶのはハンドルの複製(`handle_dup`)など libfnd に露出していない 1 対 1 の操作だけです。 +5. **GPL のコードは含めない**。BusyBox(GPL-2.0)をこのリポジトリに置きません(§8.1)。 + +## 2. 全体像 + +``` +BusyBox / POSIX アプリ (外部ソース) + │ POSIX 呼び出し +libc(stdio、string、qsort…) (Newlib を予定、§9) + │ POSIX I/O・プロセス・時刻など(open/read/write/stat/posix_spawn…) +pkcl foundation/lib/pkcl(静的ライブラリ) + │ fnd_file_open / fnd_read / fnd_proc_exec … +libfnd(WAL fnd) foundation/lib/fnd + │ IPC(ipc_call)と syscall +Foundation カーネル・fs・console サービス +``` + +fdio と同じく、pkcl の仕事は「ファイル記述子の表」と「POSIX の意味をサービスのプロトコルに合わせること」です。ストリームの位置・権限・パス解決はサービス(fs)が持ち、pkcl は持ちません(API §6.2:パスは書き換えずに送る)。 + +## 3. 構成 + +### 3.1 ファイル + +| 場所 | 内容 | +|---|---| +| `foundation/include/pkcl/` | POSIX ヘッダ(`errno.h`、`fcntl.h`、`unistd.h`、`sys/types.h`、`sys/stat.h`…)と内部境界 `pkcl/pkcl.h` | +| `foundation/lib/pkcl/` | 実装(`errno.c`、`fd.c`、`fs.c`、`crt.c`) | +| `foundation/tests/pkcl/` | 実際の fs・console に対するゲストテスト | + +### 3.2 ビルド + +pkcl は**ソースを一緒にコンパイルして静的にリンク**します(WAL にはしません)。ファイル記述子の表という可変な大域データを持ち、WAL の静的データ制約(アドレスを含むデータは不可)に触れずに済ませるためです。標準の指定は次のとおりです。 + +``` +python tools/llvm-src32/driver.py app.c foundation/lib/pkcl/*.c \ + -I foundation/include/pkcl -I foundation/include -I sdk/cpt32/include \ + --entry pkcl_start --wax3 --import-wal root/lib/fnd.1.wal ... +``` + +- `-I foundation/include/pkcl` を**先頭**に置き、libc のヘッダより先に pkcl のヘッダを見せます(libc 統合後は §9 で調整)。 +- WAL `fnd` を import する WAX v3 が基本です。静的 libfnd(WAX v2)でも動きます。 + +### 3.3 起動 + +`--entry pkcl_start` を指定すると、`crt.c` の `pkcl_start` が `fnd_init`、`pkcl_init`(fd 0〜2 の準備)、argv の組み立てを行い、`int main(int argc, char **argv)` を呼んで、その戻り値で `fnd_exit` します。`environ` は現状空です(§7 G2)。`pkcl_init` は POSIX 呼び出しの先頭でも遅延して呼ばれるので、`main(void)` と `fnd_init` の既存形式のアプリでも pkcl の関数を使えます。 + +## 4. 型と ABI の方針 + +| 型 | 定義 | 理由 | +|---|---|---| +| `off_t` | `int32_t` | `llvm-src32` の `i64` は使えるが、BusyBox は `CONFIG_LFS=n` で組める。FS2 は 64 bit のサイズを持つので、2 GiB 以上は `EOVERFLOW`。64 bit 化は §10 M5 | +| `time_t` | `int64_t` | fnd_stat の時刻は i64 秒 | +| `mode_t`、`uid_t`、`gid_t`、`pid_t` | 32 bit | | +| `errno` | 単一のグローバル `int errno` | スレッドごとにはしない(スレッドは `thread_create` で作れるが pkcl のスレッド対応は M5) | +| 数値定数 | Newlib の `sys/errno.h`、`_default_fcntl.h` と同じ値 | libc を Newlib にしたとき食い違わないため | + +## 5. エラーの変換 + +Foundation の `fnd_status`(負値)を `pkcl_errno` で errno に直します。 + +| Foundation | errno | +|---|---| +| `BAD_CALL`、`UNSUPPORTED` | `ENOSYS` | +| `BAD_ARG`、`BAD_PATH` | `EINVAL` | +| `BAD_HANDLE` | `EBADF` | +| `ACCESS` | `EACCES` | +| `NO_MEMORY`、`ADDRESS_CONFLICT`、`REGION_LIMIT` | `ENOMEM` | +| `LIMIT` | `EMFILE`(`getcwd` の容量不足は `ERANGE`) | +| `TIMEOUT` | `ETIMEDOUT` | +| `WOULD_BLOCK` | `EAGAIN` | +| `NOT_FOUND`、`DEPENDENCY` | `ENOENT` | +| `EXISTS` | `EEXIST` | +| `FORMAT` | `ENOEXEC` | +| `NO_MEDIA` | `ENODEV` | +| `READ_ONLY` | `EROFS` | +| `BUSY` | `EBUSY` | +| `KILLED`、`INTERRUPTED` | `EINTR` | +| `NOT_DIR` / `IS_DIR` / `NOT_EMPTY` | `ENOTDIR` / `EISDIR` / `ENOTEMPTY` | +| `NAME_TOO_LONG` | `ENAMETOOLONG` | +| `NO_SPACE` | `ENOSPC` | +| `CROSS_DEVICE` | `EXDEV` | +| 上記以外の負値 | `EIO` | + +`FND_E_INTERRUPTED`(Ctrl+C による入力待ちの中断、API §8.4)が `EINTR` になるので、BusyBox のシグナル処理(§6.5)と接続できます。 + +## 6. POSIX 面の対応表 + +「状態」:**実装済み**(v0.1)/**予定**(Issue あり、Foundation の変更なし)/**要変更**(§7 のギャップが先)/**不可**(恒久的にエラー)。 + +### 6.1 ファイル記述子と I/O + +| 関数 | 方式 | 状態 | +|---|---|---| +| `open`、`creat`、`close`、`read`、`write` | `fnd_file_open`/`fnd_read`/`fnd_write`/`handle_close`。1 記述子 = 1 CHANNEL | 実装済み | +| `lseek` | `ST_SEEK`。コンソール・シーク不可は `ESPIPE`。32 bit を超える位置は `EOVERFLOW` | 実装済み | +| `dup`、`dup2`、`fcntl(F_DUPFD/GETFD/SETFD/GETFL/SETFL)` | `handle_dup`。位置はサービスが持つので dup 間で共有される(POSIX どおり) | 実装済み(`dup3`、`F_DUPFD_CLOEXEC` は予定) | +| `ftruncate`、`truncate`、`fsync` | `ST_TRUNCATE`、`ST_SYNC` | 実装済み | +| `isatty` | `ST_STAT` の type = `CONSOLE` | 実装済み | +| `fstat` | `ST_STAT` | 実装済み | +| `pread`、`pwrite`、`readv`、`writev` | `lseek` + read/write で代用できるが原子的ではない | 予定 | +| `pipe`、`pipe2`、`mkfifo` | ユーザー空間の `pipe` サービス(IPC +ハンドル転送。カーネル変更なし)。§7 G3 | 予定 | +| `select`、`poll` | コンソールは `CON_READ_KEY(NOWAIT)`、ファイルは常に ready、pipe はサービスの op | 予定 | +| `mmap`、`munmap` | 無い。`MAP_PRIVATE` ファイル読み込みだけは read で模倣できるが、BusyBox は不要 | 不可 | +| `fcntl(F_SETLK)`、`flock` | ロックなし | 不可(`ENOSYS`) | + +`O_NONBLOCK` は記録するだけで、コンソールの `read` は常にブロックします(M4 の `poll` と一緒に扱う)。`O_APPEND` は `FND_OPEN_APPEND`(サービス側)で実現し、`F_SETFL` で後から変えても効きません。 + +### 6.2 パスとディレクトリ + +| 関数 | 方式 | 状態 | +|---|---|---| +| `stat`、`lstat`(= `stat`)、`access` | `fnd_path_stat`。§6.3 の対応 | 実装済み | +| `mkdir`、`rmdir`、`unlink`、`rename` | `fnd_dir_make` 等。`unlink(ディレクトリ)` は `EISDIR`。`rename` は置換先が通常ファイルのときだけ「削除してから rename」で上書きを模倣(途中失敗で両方を失う窓がある。ディレクトリへの上書きは `EEXIST` のまま) | 実装済み | +| `chdir`、`getcwd` | `fnd_cwd_set`/`fnd_cwd_get`(cwd は fs のセッションが持つ) | 実装済み | +| `chmod`、`umask` | `FS_CHMOD`。`umask` は pkcl が保持して `open`/`mkdir` の mode に適用(FS2 に umask はない) | 実装済み | +| `opendir`、`readdir`、`rewinddir`、`closedir`、`dirfd` | `fnd_dir_open`/`fnd_dir_next`/`fnd_dir_rewind`。`d_ino` は 0、`d_type` は stat の type から | 予定(M1) | +| `open(ディレクトリ, O_RDONLY)` | 現状は `EISDIR`(`FS_OPEN` の仕様)。BusyBox は `opendir` を使う | 予定(`O_DIRECTORY`、M1) | +| `chown`、`fchown` | UID は 0 固定なので、所有者を変えない呼び出しだけ成功 | 予定 | +| `utime`、`utimes`、`futimens` | `FS_UTIME` がない。§7 G5。それまで `touch` は新規作成だけ可能(pkcl は `ENOSYS`) | 要変更 | +| `symlink`、`link`、`readlink` | FS2 にない | 不可(`ENOSYS`) | +| `realpath` | libc の実装(`getcwd` + `lstat`)に任せる | 予定 | +| `statvfs`、`statfs` | `FS_VOLUME_INFO` | 予定(`df` 用) | + +### 6.3 `struct stat` の対応 + +| POSIX | Foundation | +|---|---| +| `st_mode` の型 | `FILE`→`S_IFREG`、`DIR`→`S_IFDIR`、`DEVICE`/`CONSOLE`→`S_IFCHR` | +| `st_mode` の権限 | FS2 の owner 3 bit と other 3 bit(FS2 §8)→ `user` と `other`。`group` は other と同じに見せる。逆方向(`chmod`、`open(O_CREAT)`、`mkdir`)は group を捨てる | +| 読み取り専用の mount | `FND_STAT_READ_ONLY` のとき W bit を落とす | +| `st_size` | `size_lo`(`size_hi` ≠ 0 や 2 GiB 以上は `EOVERFLOW`) | +| `st_mtime`、`st_ctime` | `modified_sec`、`changed_sec`(`atime` は mtime と同じ) | +| `st_ino`、`st_dev` | 0、1。**FS2 に inode を返す欄がない**ので、BusyBox の「同じファイルか」の判定(`cp`、`mv`)は偽陽性になりうる。§7 G4 | +| `st_nlink`、`st_gid`、`st_uid` | 1、0、`uid` | + +### 6.4 プロセス + +Foundation のプロセスは `fnd_proc_exec`(= `proc_spawn`)で作る新しいイメージで、`fork` に相当する操作はありません(メモリのコピーも COW の仕組みもプロセスの複製にはない)。 + +| 関数 | 方式 | 状態 | +|---|---|---| +| `_exit`、`exit` | `fnd_exit`(`exit` と `atexit` は libc) | `_exit` 実装済み | +| `posix_spawn`、`posix_spawnp` | `fnd_proc_exec`。file actions(`dup2`/`close`/`open`)は子の `std_in/out/err` に渡すハンドルへ翻訳(0〜2 だけ)。`posix_spawnp` は `PATH` を `fnd_path_stat` で探す | 予定(M2) | +| `waitpid`、`wait` | pkcl の子プロセス表(pid → PROCESS ハンドル)と `fnd_proc_wait`。`WNOHANG` は `wait(h, 0, …)`(`WOULD_BLOCK`→0) | 予定(M2) | +| `execve`、`execv`、`execvp` | **代理 exec**:子を spawn して待ち、その終了コードで `_exit`。fd の継承は 0〜2 のみ。自分のハンドル(fd 3 以上)は渡らない | 予定(M2) | +| `system`、`popen` | `posix_spawn("/bin/sh")` + `waitpid`(`popen` は pipe 後) | 予定 | +| `fork`、`vfork` | **不可**(`ENOSYS`)。BusyBox は NOMMU 構成と `posix_spawn` への置き換えで回避する(§8.3) | 不可 | +| `getpid`、`getppid` | カーネルに「自分の pid」を返す手段がない。§7 G1 | 要変更 | +| `kill`、`signal`、`sigaction`、`raise` | 非同期シグナルはない。§6.5 | 予定(M4) | +| `sleep`、`nanosleep`、`usleep` | `sleep`(tick は 10 ms 単位に切り上げ) | 予定(M4) | +| `getuid` 系 | 常に 0 | 実装済み | +| `setuid` 系、`chroot` | | 不可(`EPERM`/`ENOSYS`) | +| `getenv`、`setenv` | libc。環境の継承は §7 G2 | 要変更 | + +### 6.5 シグナル + +カーネルに非同期シグナルはありません。pkcl は次のことだけをエミュレーションします。 + +- `signal`/`sigaction` はハンドラを pkcl の表に記録する。`raise` と `kill(getpid(), sig)` はそのハンドラを同期して呼ぶ。 +- `SIGINT`:`CON_BREAK_WAIT`(API §8.4)を待つ専用スレッドを pkcl が 1 本作り、Ctrl+C でハンドラを呼ぶ(呼び出しはそのスレッドの上なので、ハンドラは `volatile` のフラグを立てるだけにする。BusyBox の ash/hush はそう書かれている)。ブロック中の `read` は `FND_E_INTERRUPTED` → `EINTR` で戻る。 +- `kill(pid, SIGKILL/SIGTERM)`:pkcl が spawn した子なら `fnd_proc_kill`。 +- `SIGCHLD`、`SIGPIPE`、`alarm`、タイマーは作らない。 + +### 6.6 端末(termios) + +`tcgetattr`/`tcsetattr` は、コンソールの 2 つの入力モードへ翻訳します。 + +| termios | コンソール | +|---|---| +| `ICANON` オン | `LINE`(行編集あり、エコーあり) | +| `ICANON` オフ | `RAW`(キーはそのまま、エコーなし。エコーするかはアプリ) | +| `ECHO`、`ISIG`、`VMIN/VTIME` | `ECHO` だけ off にできない(`LINE` では常にエコー)。`ISIG` は §6.5。`VMIN/VTIME` は `poll` との組み合わせ(M4) | + +`ioctl(TIOCGWINSZ)` は `CON_GET_SIZE`。キー入力は `FND_KEY_*` から VT100 風のバイト列(矢印は `ESC [ A`…)に変換して `read` に返す必要がある(`vi` や `ash` の行編集用)。これは M4 の項目です。 + +### 6.7 時刻 + +| 関数 | 方式 | +|---|---| +| `time`、`gettimeofday`、`clock_gettime(CLOCK_REALTIME)` | `time_get(3, 4)`(RTC 秒)。ミリ秒は tick から補完 | +| `clock_gettime(CLOCK_MONOTONIC)`、`times`、`clock` | `time_get(0, 1)`(tick。`tick_hz` で換算し 1 tick = 1 ms と仮定しない) | +| `localtime`、`strftime` | libc。タイムゾーンは UTC 固定 | + +## 7. Foundation 側のギャップ(pkcl が待つもの) + +pkcl の方針は「少しの変更なら入れる」です。次は、それぞれ独立した小さな変更の候補です。**この PR では実装しません**(Issue にします)。 + +| ID | 欠けているもの | 提案する最小の変更 | 影響する POSIX | +|---|---|---|---| +| G1 | 自分の pid を知る手段 | `sys_info` に sel を 1 つ足す(自プロセスの pid と parent) | `getpid`、`getppid`、`kill(getpid())` | +| G2 | 環境変数の受け渡し | **カーネルは起動ブロックを解釈しない**(Foundation §9.3)ので、libfnd の規約として起動ブロック version 2 に `env_off`/`env_count` を足す。`fnd_exec_t` に `envp` を足す | `environ`、`getenv`、`execve` の環境 | +| G3 | パイプ | ユーザー空間の `pipe` サービス(`svc_register`、`FS_OPEN` 風の op で 2 本のハンドルを `XFER_HANDLE` で返す)。カーネル変更なし | `pipe`、`|`、`popen`、`mkfifo` | +| G4 | inode 番号 | `fnd_stat` の予約欄(offset 60)に FS2 の inode を入れる(FS2 の inode 番号。実装時に FS2 仕様で確認する) | `st_ino`、`cp`/`mv` の同一ファイル判定 | +| G5 | 時刻の更新 | `FS_UTIME` op(path、mtime、atime)。FS2 は mtime を持つ | `touch`、`cp -p`、`utimes` | +| G6 | シンボリックリンク/ハードリンク | FS2 の拡張が必要(影響大)。当面入れない | `ln`、`readlink` | +| G7 | `setjmp`/`longjmp` | `tools/llvm-src32` の README に言及がなく、対応は未確認。ash が必須。未対応なら runtime のアセンブリ関数で実装する | ash、`libc` 全般 | +| G8 | 64 bit のファイルオフセット | `off_t` を 64 bit にする(i64 は対応済み)。API の `fnd_seek` を libfnd に足す(現在は pkcl が `ST_SEEK` を直接呼ぶ) | 2 GiB 超のファイル | +| G9 | `ST_READ` のノンブロッキング | コンソールはあるが、pipe と通常のストリームにない | `O_NONBLOCK`、`poll` | + +## 8. BusyBox への道 + +### 8.1 取り込み方針(GPL) + +BusyBox は GPL-2.0 なので、**リポジトリには含めません**。Newlib(`--newlib-source`)と同じく、利用者が公式ソースを別途用意し、パスを指定してビルドします。リポジトリに置くのは、pkcl・ビルド用のスクリプト・`.config` の差分(設定値の一覧)・ドキュメントだけです。BusyBox のソースへの patch は GPL の対象になるため、同梱せず、必要なら別リポジトリに置きます。patch を要しない構成(設定、`-D` オプション、pkcl 側の互換関数)を優先します。 + +### 8.2 必要な POSIX 面 + +最初の対象 applet を `cat`、`echo`、`ls`、`cp`、`mv`、`rm`、`mkdir`、`rmdir`、`head`、`tail`、`wc`、`sort`、`uniq`、`grep`、`sed`、`sleep`、`true`、`false`、`pwd`、`env`、`basename`、`dirname`、`tr`、`cut`、`od`/`hexdump`、`test`、`printf`、`ash`(または `hush`)とします。必要な面と pkcl の状態: + +| 必要なもの | 状態 | +|---|---| +| ファイル I/O、`stat`、`mkdir` 等 | 実装済み(§6.1〜6.3) | +| `DIR`(`ls`、`cp -r`、`rm -r`) | M1 | +| libc:`stdio`(`printf` 系、`fopen`、バッファ)、`strtol`、`qsort`、`getopt`、`regcomp`(`grep`、`sed`)、`locale` の最小 | M2(libc 統合) | +| `setjmp`/`longjmp`(ash が必須) | G7 | +| `posix_spawn`、`waitpid`(外部コマンド、`xargs`、`find -exec`) | M3 | +| `pipe`(シェルのパイプ、`popen`) | M4 | +| `termios`、`ioctl`、`signal`(対話シェル、`vi`、`top`) | M4 | +| `getpwnam` 等 | BusyBox の `USE_BB_PWD_GRP` で自前実装を使い、`/etc/passwd` を読む | +| 浮動小数点 | `llvm-src32` が SoftFloat で対応済み。BusyBox は `awk`、`dc` 以外で不要 | + +### 8.3 `fork` を使わない構成 + +BusyBox の NOMMU 構成(`CONFIG_NOMMU`)と NOFORK/NOEXEC applet(`FEATURE_PREFER_APPLETS`、`FEATURE_SH_NOFORK`、`FEATURE_SH_STANDALONE`)を使い、`fork` を呼ぶ経路を無くします。外部コマンドは `posix_spawn("/bin/busybox", argv…)` の再実行(`BB_FORCE_APPLETS`風)で起動します。**パイプラインは複数プロセスを並行して動かし、pipe サービス越しにつなぐ**ことで実現します。ash の NOMMU サポートは「`vfork` + exec」を前提としているため、`vfork` が使えない場合に ash を使えるかは M5 の調査項目です(hush は NOMMU でもより素直)。 + +### 8.4 ビルド + +`tools/pkcl/busybox.py`(予定)が、`--busybox-source PATH` の外部ソース、pkcl、libc を `tools/llvm-src32/driver.py` に渡して `busybox.wax` を作り、`/bin/busybox.wax` と各 applet 名のファイルをディスクに置きます(シンボリックリンクがないので、applet ごとに 1 つの小さな WAX で `busybox` を spawn し引数を足す、または shell 側のディスパッチ。`foundation/bin` の方式に合わせる)。WAX は 1 MiB の `FND_EXEC_IMAGE_MAX` に収まる必要があります(BusyBox は applet を絞って組む)。 + +## 9. libc の方針 + +`tools/llvm-src32` の Newlib 統合は、現状は文字列・整数・`qsort`・nano allocator だけで、`printf`/stdio/ファイル I/O は未対応です(`examples/newlib-c/README.md`)。pkcl は libc を持たず、次の順で進めます。 + +1. **M2**:Newlib の stdio を載せられるか調査する。必要なのは `_open_r`/`_read_r`/`_write_r`/`_close_r`/`_lseek_r`/`_fstat_r`/`_isatty_r`/`_sbrk_r` などの reent 層で、これは pkcl の関数へ 1 対 1 の薄いアダプタ(`pkcl/newlib_syscalls.c`、予定)にできます。障害になりそうな `llvm-src32` の未対応事項(`_impure_ptr`、TLS、`setjmp`、atomic)は Issue にします。 +2. 載せられない場合の代替:`printf` 系だけ自前の最小実装(`sol/cpt32` の C 版や `sdk/cpt32/src/print.c` 相当)を pkcl の `libc-lite` として持つ。 +3. pkcl のヘッダは libc のヘッダと衝突しないよう、`errno`、`fcntl`、`unistd`、`sys/stat`、`sys/types` だけを持ち、`stdio.h` などは libc のものを使います。 + +## 10. マイルストーンと Issue + +| マイルストーン | 内容 | 状態 | +|---|---|---| +| M0 | 設計、ヘッダ、fd I/O、パス操作、errno、起動コード、ゲストテスト | **この PR** | +| M1 | ディレクトリ(`DIR`)、`O_DIRECTORY`、`statvfs`、`chown`、`dup3`、`pread`/`pwrite` | 予定 | +| M2 | libc 統合(Newlib stdio を調査)、`posix_spawn`・`waitpid`・代理 `execve` | 予定 | +| M3 | 環境変数(G2)、`getpid`(G1)、時刻、`sleep` | 予定 | +| M4 | pipe サービス(G3)、`poll`、シグナル、termios | 予定 | +| M5 | BusyBox のビルド統合(`--busybox-source`)、最初の applet、ash/hush の調査、`setjmp`(G7) | 予定 | +| M6 | 64 bit オフセット(G8)、`utime`(G5)、inode(G4) | 予定 | + +各 Issue は GitHub Issues にあります。GitHub のマイルストーンは分野の分類(Foundation OS)で、上の M は Issue タイトルの接頭辞に書きます。 + +## 11. テスト + +`foundation/tests/pkcl/` は実際の Foundation の fs・console サービスに対して、pkcl の POSIX 呼び出しを検証します。 + +``` +python3 -m unittest discover -s foundation/tests/pkcl -v +``` + +- Clang と `llvmlite` の LLVM のメジャー版を合わせてください(`CLANG` で指定)。`src32_testbench` は release ビルドを使います(`SRC32_TESTBENCH`)。 +- ゲストは `--entry pkcl_start` で `main(argc, argv)` を持ち、失敗は失敗した行番号で終了します。テストは `pkcl guest passed` の出力と `status=0` を確認します。 + +## 12. 未決事項 + +| 論点 | 現在の案 | +|---|---| +| `fork` を要求するソフトウェア | 動かさない。BusyBox は NOMMU 構成で回避(§8.3) | +| pkcl の WAL 化 | 当面は静的リンク。fd 表を WAL の外に置く設計が固まってから検討 | +| スレッドごとの `errno` | 当面グローバル。`thread_create` を使う BusyBox の機能はない | +| group の扱い | FS2 は owner と other だけ。`st_mode` の group は other のコピー | +| `unlink` 中のオープンファイル | FS2 が BUSY を返すかを確認する(POSIX は削除後も開いたままを許す) | diff --git a/foundation/include/pkcl/errno.h b/foundation/include/pkcl/errno.h new file mode 100644 index 0000000..47460d8 --- /dev/null +++ b/foundation/include/pkcl/errno.h @@ -0,0 +1,46 @@ +#ifndef PKCL_ERRNO_H +#define PKCL_ERRNO_H + +/* pkcl の errno(doc/spec_pkcl.md §5)。値は Newlib の sys/errno.h と同じで、 + * 後から Newlib を載せても数値が食い違わない。単一のグローバルで、スレッドごとではない。 */ +extern int errno; + +#define EPERM 1 +#define ENOENT 2 +#define EINTR 4 +#define EIO 5 +#define ENOEXEC 8 +#define EBADF 9 +#define ECHILD 10 +#define EAGAIN 11 +#define EWOULDBLOCK EAGAIN +#define ENOMEM 12 +#define EACCES 13 +#define EFAULT 14 +#define EBUSY 16 +#define EEXIST 17 +#define EXDEV 18 +#define ENODEV 19 +#define ENOTDIR 20 +#define EISDIR 21 +#define EINVAL 22 +#define ENFILE 23 +#define EMFILE 24 +#define ENOTTY 25 +#define EFBIG 27 +#define ENOSPC 28 +#define ESPIPE 29 +#define EROFS 30 +#define EPIPE 32 +#define EDOM 33 +#define ERANGE 34 +#define ENOSYS 88 +#define ENOTEMPTY 90 +#define ENAMETOOLONG 91 +#define ELOOP 92 +#define EPROTO 71 +#define ENOTSUP 134 +#define EOVERFLOW 139 +#define ETIMEDOUT 116 + +#endif diff --git a/foundation/include/pkcl/fcntl.h b/foundation/include/pkcl/fcntl.h new file mode 100644 index 0000000..aea3b0b --- /dev/null +++ b/foundation/include/pkcl/fcntl.h @@ -0,0 +1,29 @@ +#ifndef PKCL_FCNTL_H +#define PKCL_FCNTL_H +#include + +/* 値は Newlib の sys/_default_fcntl.h と同じ。 */ +#define O_RDONLY 0 +#define O_WRONLY 1 +#define O_RDWR 2 +#define O_ACCMODE 3 +#define O_APPEND 0x0008 +#define O_CREAT 0x0200 +#define O_TRUNC 0x0400 +#define O_EXCL 0x0800 +#define O_NONBLOCK 0x4000 +#define O_CLOEXEC 0x40000 +#define O_DIRECTORY 0x200000 + +#define F_DUPFD 0 +#define F_GETFD 1 +#define F_SETFD 2 +#define F_GETFL 3 +#define F_SETFL 4 +#define FD_CLOEXEC 1 + +int open(const char *path, int flags, ...); +int creat(const char *path, mode_t mode); +int fcntl(int fd, int cmd, ...); + +#endif diff --git a/foundation/include/pkcl/pkcl.h b/foundation/include/pkcl/pkcl.h new file mode 100644 index 0000000..a6750b4 --- /dev/null +++ b/foundation/include/pkcl/pkcl.h @@ -0,0 +1,33 @@ +#ifndef PKCL_PKCL_H +#define PKCL_PKCL_H +#include +#include + +/* pkcl の内部境界(doc/spec_pkcl.md §3)。アプリケーションは POSIX の名前だけを使い、 + * これは pkcl の .c 同士と、pkcl をテストするコードのためのものです。 */ + +/* Foundation のエラーを errno の値に直す。0 以上(成功)は 0。 */ +int pkcl_errno(fnd_status status); +/* status が負なら errno を設定して -1、そうでなければ 0 を返す。 */ +int pkcl_fail(fnd_status status); + +/* fd が開いていれば Foundation のハンドル、そうでなければ 0(errno = EBADF)。 */ +fnd_handle pkcl_handle(int fd); + +/* 最初の POSIX 呼び出しで自動的に呼ばれる。fnd_init と fd 0〜2 の準備をする。 */ +void pkcl_init(void); + +/* 絶対パス化せずにそのまま fs へ送るので、パス長(NUL を除く)の上限だけ検査する。 */ +#define PKCL_PATH_MAX 4096 + +/* POSIX の mode と FS2 の mode(owner と other の 6 bit、spec_FS2 §8)の変換。 + * group の bit は捨て、stat では other と同じ値にする。 */ +uint32_t pkcl_mode_to_fnd(uint32_t posix_mode); +uint32_t pkcl_mode_from_fnd(uint32_t type, uint32_t fnd_mode); + +/* fnd_stat を struct stat に直す。大きさが 2 GiB 以上なら EOVERFLOW で -1。 */ +int pkcl_stat_convert(const fnd_stat_t *fs, struct stat *st); +/* umask の現在値(umask() は設定と取得を同時に行うため)。 */ +mode_t pkcl_current_umask(void); + +#endif diff --git a/foundation/include/pkcl/sys/stat.h b/foundation/include/pkcl/sys/stat.h new file mode 100644 index 0000000..98c163d --- /dev/null +++ b/foundation/include/pkcl/sys/stat.h @@ -0,0 +1,52 @@ +#ifndef PKCL_SYS_STAT_H +#define PKCL_SYS_STAT_H +#include + +#define S_IFMT 0170000 +#define S_IFDIR 0040000 +#define S_IFCHR 0020000 +#define S_IFREG 0100000 +#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) +#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) +#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) +#define S_ISBLK(m) 0 +#define S_ISFIFO(m) 0 +#define S_ISLNK(m) 0 +#define S_ISSOCK(m) 0 + +#define S_IRUSR 0400 +#define S_IWUSR 0200 +#define S_IXUSR 0100 +#define S_IRWXU 0700 +#define S_IRGRP 0040 +#define S_IWGRP 0020 +#define S_IXGRP 0010 +#define S_IRWXG 0070 +#define S_IROTH 0004 +#define S_IWOTH 0002 +#define S_IXOTH 0001 +#define S_IRWXO 0007 + +/* FS2 に inode 番号・リンク数・GID・atime はない。st_ino は 0、st_nlink は 1、 + * st_gid は 0、st_atime は st_mtime と同じ(doc/spec_pkcl.md §6.3)。 */ +struct stat { + dev_t st_dev; + ino_t st_ino; + mode_t st_mode; + nlink_t st_nlink; + uid_t st_uid; + gid_t st_gid; + off_t st_size; + time_t st_atime; + time_t st_mtime; + time_t st_ctime; +}; + +int stat(const char *path, struct stat *st); +int lstat(const char *path, struct stat *st); +int fstat(int fd, struct stat *st); +int mkdir(const char *path, mode_t mode); +int chmod(const char *path, mode_t mode); +mode_t umask(mode_t mask); + +#endif diff --git a/foundation/include/pkcl/sys/types.h b/foundation/include/pkcl/sys/types.h new file mode 100644 index 0000000..7577f70 --- /dev/null +++ b/foundation/include/pkcl/sys/types.h @@ -0,0 +1,19 @@ +#ifndef PKCL_SYS_TYPES_H +#define PKCL_SYS_TYPES_H +#include +#include + +/* off_t は 32 bit(2 GiB 未満)。BusyBox は CONFIG_LFS=n で組む。 + * 大きなオフセットは EOVERFLOW になる(doc/spec_pkcl.md §4)。 */ +typedef int32_t ssize_t; +typedef int32_t off_t; +typedef uint32_t mode_t; +typedef uint32_t uid_t; +typedef uint32_t gid_t; +typedef int32_t pid_t; +typedef uint32_t dev_t; +typedef uint32_t ino_t; +typedef uint32_t nlink_t; +typedef int64_t time_t; + +#endif diff --git a/foundation/include/pkcl/unistd.h b/foundation/include/pkcl/unistd.h new file mode 100644 index 0000000..4b4dd51 --- /dev/null +++ b/foundation/include/pkcl/unistd.h @@ -0,0 +1,44 @@ +#ifndef PKCL_UNISTD_H +#define PKCL_UNISTD_H +#include + +#define STDIN_FILENO 0 +#define STDOUT_FILENO 1 +#define STDERR_FILENO 2 + +#define SEEK_SET 0 +#define SEEK_CUR 1 +#define SEEK_END 2 + +#define F_OK 0 +#define X_OK 1 +#define W_OK 2 +#define R_OK 4 + +/* ファイル記述子は 0..PKCL_OPEN_MAX-1。カーネルのハンドル上限(63)に合わせる。 */ +#define PKCL_OPEN_MAX 64 + +int close(int fd); +ssize_t read(int fd, void *buf, size_t len); +ssize_t write(int fd, const void *buf, size_t len); +off_t lseek(int fd, off_t offset, int whence); +int dup(int fd); +int dup2(int fd, int newfd); +int isatty(int fd); +int ftruncate(int fd, off_t length); +int truncate(const char *path, off_t length); +int fsync(int fd); +int access(const char *path, int mode); +int unlink(const char *path); +int rmdir(const char *path); +int rename(const char *old_path, const char *new_path); +int chdir(const char *path); +char *getcwd(char *buf, size_t size); +void _exit(int code) __attribute__((noreturn)); + +uid_t getuid(void); +uid_t geteuid(void); +gid_t getgid(void); +gid_t getegid(void); + +#endif diff --git a/foundation/lib/pkcl/crt.c b/foundation/lib/pkcl/crt.c new file mode 100644 index 0000000..93b5eeb --- /dev/null +++ b/foundation/lib/pkcl/crt.c @@ -0,0 +1,24 @@ +/* pkcl の起動コード(doc/spec_pkcl.md §3.3)。`--entry pkcl_start` で指定する。 + * 起動ブロックから argc/argv を取り出し、`int main(int, char **)` を呼んで、 + * 戻り値で終了する。 */ +#include +#include + +extern int main(int argc, char **argv); + +/* 環境変数。起動ブロックにはまだ環境がない(doc/spec_pkcl.md §7 G2)ので空から始める。 */ +static char *empty_environ[1]; +char **environ = empty_environ; + +void pkcl_start(void) { + fnd_init(); + pkcl_init(); + uint32_t argc = fnd_argc(); + char **argv = fnd_mem_alloc((argc + 1u) * sizeof(char *)); + if (!argv) + fnd_exit(FND_E_NO_MEMORY); + for (uint32_t i = 0; i < argc; ++i) + argv[i] = (char *)fnd_argv(i); /* 起動ブロックは書き換え可能なヒープ上にある */ + argv[argc] = 0; + fnd_exit(main((int)argc, argv)); +} diff --git a/foundation/lib/pkcl/errno.c b/foundation/lib/pkcl/errno.c new file mode 100644 index 0000000..23a4458 --- /dev/null +++ b/foundation/lib/pkcl/errno.c @@ -0,0 +1,68 @@ +/* pkcl の errno と変換表(doc/spec_pkcl.md §5)。 */ +#include +#include +#include + +int errno; + +int pkcl_errno(fnd_status status) { + switch (status) { + case FND_E_BAD_CALL: + case FND_E_UNSUPPORTED: + return ENOSYS; + case FND_E_BAD_ARG: + case FND_E_BAD_PATH: + return EINVAL; + case FND_E_BAD_HANDLE: + return EBADF; + case FND_E_ACCESS: + return EACCES; + case FND_E_NO_MEMORY: + case FND_E_ADDRESS_CONFLICT: + case FND_E_REGION_LIMIT: + return ENOMEM; + case FND_E_LIMIT: + return EMFILE; + case FND_E_TIMEOUT: + return ETIMEDOUT; + case FND_E_WOULD_BLOCK: + return EAGAIN; + case FND_E_NOT_FOUND: + case FND_E_DEPENDENCY: + return ENOENT; + case FND_E_EXISTS: + return EEXIST; + case FND_E_FORMAT: + return ENOEXEC; + case FND_E_NO_MEDIA: + return ENODEV; + case FND_E_READ_ONLY: + return EROFS; + case FND_E_BUSY: + return EBUSY; + case FND_E_KILLED: + case FND_E_INTERRUPTED: + return EINTR; + case FND_E_NOT_DIR: + return ENOTDIR; + case FND_E_IS_DIR: + return EISDIR; + case FND_E_NOT_EMPTY: + return ENOTEMPTY; + case FND_E_NAME_TOO_LONG: + return ENAMETOOLONG; + case FND_E_NO_SPACE: + return ENOSPC; + case FND_E_CROSS_DEVICE: + return EXDEV; + default: + return status >= 0 ? 0 : EIO; /* PEER_GONE, IO, NEEDS_FSCK と未知のコード */ + } +} + +int pkcl_fail(fnd_status status) { + if (status >= 0) + return 0; + errno = pkcl_errno(status); + return -1; +} diff --git a/foundation/lib/pkcl/fd.c b/foundation/lib/pkcl/fd.c new file mode 100644 index 0000000..892273b --- /dev/null +++ b/foundation/lib/pkcl/fd.c @@ -0,0 +1,316 @@ +/* ファイル記述子表と、ストリームに対する POSIX 呼び出し(doc/spec_pkcl.md §6.1)。 + * 1 つの記述子は 1 つの Foundation ハンドル(開いたファイル 1 つ = 1 CHANNEL)を指し、 + * 読み書き位置はサービス側が持つ。dup はハンドルの複製なので、位置を共有する(POSIX どおり)。 */ +#include +#include +#include +#include +#include +#include +#include +#include + +struct fd_entry { + fnd_handle handle; /* 0 = 未使用 */ + uint32_t status; /* F_GETFL: アクセスモードと O_APPEND、O_NONBLOCK */ + uint32_t cloexec; /* FD_CLOEXEC。exec 経路ができるまで記録するだけ */ +}; + +static struct fd_entry fds[PKCL_OPEN_MAX]; +static int initialized; +static mode_t current_umask = 022; + +void pkcl_init(void) { + if (initialized) + return; + initialized = 1; + fnd_handle std[3] = {fnd_std_in(), fnd_std_out(), fnd_std_err()}; + static const uint32_t access[3] = {O_RDONLY, O_WRONLY, O_WRONLY}; + for (int i = 0; i < 3; ++i) { + fnd_handle h = std[i]; + if (!h) + continue; + /* fnd_std_err() は役割がなければ STDOUT と同じハンドルを返す。 + * 同じハンドルを 2 つの記述子で持つと、片方の close でもう片方が壊れる。 */ + for (int j = 0; j < i; ++j) { + if (fds[j].handle == h) { + int32_t copy = fnd_sys_handle_dup(h); + h = copy > 0 ? (fnd_handle)copy : 0; + break; + } + } + fds[i].handle = h; + fds[i].status = access[i]; + } +} + +fnd_handle pkcl_handle(int fd) { + pkcl_init(); + if (fd < 0 || fd >= PKCL_OPEN_MAX || !fds[fd].handle) { + errno = EBADF; + return 0; + } + return fds[fd].handle; +} + +/* from 以上で最小の空き記述子。なければ -1(EMFILE)。 */ +static int fd_alloc(int from) { + for (int fd = from; fd < PKCL_OPEN_MAX; ++fd) { + if (!fds[fd].handle) + return fd; + } + errno = EMFILE; + return -1; +} + +uint32_t pkcl_mode_to_fnd(uint32_t m) { + /* FS2: bit 0〜2 が other の X/W/R、bit 3〜5 が owner の X/W/R。 */ + return ((m >> 6) & 7u) << 3 | (m & 7u); +} + +uint32_t pkcl_mode_from_fnd(uint32_t type, uint32_t f) { + uint32_t owner = (f >> 3) & 7u, other = f & 7u; + uint32_t kind = type == FND_TYPE_DIR ? S_IFDIR : type == FND_TYPE_FILE ? S_IFREG : S_IFCHR; + /* group は持たないので other と同じに見せる。 */ + return kind | owner << 6 | other << 3 | other; +} + +int open(const char *path, int flags, ...) { + pkcl_init(); + mode_t mode = 0; + if (flags & O_CREAT) { + va_list ap; + va_start(ap, flags); + mode = va_arg(ap, mode_t); + va_end(ap); + } + uint32_t access = (uint32_t)flags & O_ACCMODE, fl = 0; + if (access == O_RDONLY) + fl = FND_OPEN_READ; + else if (access == O_WRONLY) + fl = FND_OPEN_WRITE; + else if (access == O_RDWR) + fl = FND_OPEN_READ | FND_OPEN_WRITE; + else { + errno = EINVAL; + return -1; + } + if (flags & O_CREAT) + fl |= FND_OPEN_CREATE; + if ((flags & O_EXCL) && (flags & O_CREAT)) + fl |= FND_OPEN_EXCL; + /* TRUNC は WRITE を要する(API §6.4)。O_RDONLY|O_TRUNC は POSIX では未規定なので無視する。 */ + if ((flags & O_TRUNC) && (fl & FND_OPEN_WRITE)) + fl |= FND_OPEN_TRUNC; + if ((flags & O_APPEND) && (fl & FND_OPEN_WRITE)) + fl |= FND_OPEN_APPEND; + int fd = fd_alloc(0); + if (fd < 0) + return -1; + uint32_t fnd_mode = FND_MODE_DEFAULT; + if (flags & O_CREAT) + fnd_mode = pkcl_mode_to_fnd(mode & ~current_umask); + fnd_handle h; + if (pkcl_fail(fnd_file_open(path, fl, fnd_mode, &h))) + return -1; + fds[fd].handle = h; + fds[fd].status = (uint32_t)flags & (O_ACCMODE | O_APPEND | O_NONBLOCK); + fds[fd].cloexec = (flags & O_CLOEXEC) ? FD_CLOEXEC : 0; + return fd; +} + +int creat(const char *path, mode_t mode) { return open(path, O_WRONLY | O_CREAT | O_TRUNC, mode); } + +int close(int fd) { + fnd_handle h = pkcl_handle(fd); + if (!h) + return -1; + fds[fd].handle = 0; + return pkcl_fail(fnd_close(h)); +} + +ssize_t read(int fd, void *buf, size_t len) { + fnd_handle h = pkcl_handle(fd); + if (!h) + return -1; + if ((fds[fd].status & O_ACCMODE) == O_WRONLY) { + errno = EBADF; + return -1; + } + if (len > 0x7FFFFFFFu) + len = 0x7FFFFFFFu; + uint32_t got = 0; + if (pkcl_fail(fnd_read(h, buf, (uint32_t)len, &got))) + return -1; + return (ssize_t)got; +} + +ssize_t write(int fd, const void *buf, size_t len) { + fnd_handle h = pkcl_handle(fd); + if (!h) + return -1; + if ((fds[fd].status & O_ACCMODE) == O_RDONLY) { + errno = EBADF; + return -1; + } + if (len > 0x7FFFFFFFu) + len = 0x7FFFFFFFu; + uint32_t put = 0; + fnd_status status = fnd_write(h, buf, (uint32_t)len, &put); + /* 一部だけ書けていれば、POSIX どおりその byte 数を成功として返す。 */ + if (status < 0 && put == 0) + return pkcl_fail(status); + return (ssize_t)put; +} + +off_t lseek(int fd, off_t offset, int whence) { + fnd_handle h = pkcl_handle(fd); + if (!h) + return -1; + if (whence < SEEK_SET || whence > SEEK_END) { + errno = EINVAL; + return -1; + } + int64_t delta = offset; + uint32_t args[4] = {(uint32_t)whence, (uint32_t)((uint64_t)delta >> 32), (uint32_t)delta, 0}; + fnd_msg rep; + fnd_status status = fnd_call(h, FND_ST_SEEK, args, 0, 0, 0, 0, &rep); + if (status < 0) { + /* サービスは UNSUPPORTED を返す(コンソール、シーク不可のデバイス)。POSIX では ESPIPE。 */ + errno = status == FND_E_UNSUPPORTED ? ESPIPE : pkcl_errno(status); + return -1; + } + if (rep.a0 != 0 || rep.a1 > 0x7FFFFFFFu) { + errno = EOVERFLOW; + return -1; + } + return (off_t)rep.a1; +} + +int dup(int fd) { + fnd_handle h = pkcl_handle(fd); + if (!h) + return -1; + int nfd = fd_alloc(0); + if (nfd < 0) + return -1; + int32_t copy = fnd_sys_handle_dup(h); + if (copy <= 0) + return pkcl_fail(copy); + fds[nfd] = (struct fd_entry){(fnd_handle)copy, fds[fd].status, 0}; + return nfd; +} + +int dup2(int fd, int newfd) { + fnd_handle h = pkcl_handle(fd); + if (!h) + return -1; + if (newfd < 0 || newfd >= PKCL_OPEN_MAX) { + errno = EBADF; + return -1; + } + if (fd == newfd) + return newfd; + int32_t copy = fnd_sys_handle_dup(h); + if (copy <= 0) + return pkcl_fail(copy); + if (fds[newfd].handle) + fnd_close(fds[newfd].handle); + fds[newfd] = (struct fd_entry){(fnd_handle)copy, fds[fd].status, 0}; + return newfd; +} + +int fcntl(int fd, int cmd, ...) { + fnd_handle h = pkcl_handle(fd); + if (!h) + return -1; + va_list ap; + va_start(ap, cmd); + int arg = va_arg(ap, int); + va_end(ap); + switch (cmd) { + case F_DUPFD: { + int nfd = fd_alloc(arg < 0 ? 0 : arg); + if (nfd < 0) + return -1; + int32_t copy = fnd_sys_handle_dup(h); + if (copy <= 0) + return pkcl_fail(copy); + fds[nfd] = (struct fd_entry){(fnd_handle)copy, fds[fd].status, 0}; + return nfd; + } + case F_GETFD: + return (int)fds[fd].cloexec; + case F_SETFD: + fds[fd].cloexec = (uint32_t)arg & FD_CLOEXEC; + return 0; + case F_GETFL: + return (int)fds[fd].status; + case F_SETFL: + /* アクセスモードは変えられない。O_APPEND は開く時のストリームに固定なので、 + * 後から変えても効かない(記録も更新しない)。O_NONBLOCK は記録だけする。 */ + fds[fd].status = (fds[fd].status & ~(uint32_t)O_NONBLOCK) | ((uint32_t)arg & O_NONBLOCK); + return 0; + default: + errno = EINVAL; + return -1; + } +} + +int isatty(int fd) { + fnd_handle h = pkcl_handle(fd); + fnd_stat_t st; + if (!h) + return 0; + if (fnd_stream_stat(h, &st) == FND_OK && st.type == FND_TYPE_CONSOLE) + return 1; + errno = ENOTTY; + return 0; +} + +int fsync(int fd) { + fnd_handle h = pkcl_handle(fd); + return h ? pkcl_fail(fnd_sync(h)) : -1; +} + +int ftruncate(int fd, off_t length) { + fnd_handle h = pkcl_handle(fd); + if (!h) + return -1; + if (length < 0) { + errno = EINVAL; + return -1; + } + uint32_t args[4] = {0, 0, (uint32_t)length, 0}; + fnd_status status = fnd_call(h, FND_ST_TRUNCATE, args, 0, 0, 0, 0, 0); + if (status == FND_E_UNSUPPORTED) { + errno = EINVAL; /* POSIX: 切り詰められない種類のファイル */ + return -1; + } + return pkcl_fail(status); +} + +int fstat(int fd, struct stat *st) { + fnd_handle h = pkcl_handle(fd); + fnd_stat_t fs; + if (!h) + return -1; + if (pkcl_fail(fnd_stream_stat(h, &fs))) + return -1; + return pkcl_stat_convert(&fs, st); +} + +mode_t umask(mode_t mask) { + mode_t old = current_umask; + current_umask = mask & 0777; + return old; +} + +mode_t pkcl_current_umask(void) { return current_umask; } + +void _exit(int code) { fnd_exit(code); } + +uid_t getuid(void) { return 0; } +uid_t geteuid(void) { return 0; } +gid_t getgid(void) { return 0; } +gid_t getegid(void) { return 0; } diff --git a/foundation/lib/pkcl/fs.c b/foundation/lib/pkcl/fs.c new file mode 100644 index 0000000..6e6a95a --- /dev/null +++ b/foundation/lib/pkcl/fs.c @@ -0,0 +1,138 @@ +/* パスに対する POSIX 呼び出し(doc/spec_pkcl.md §6.2〜§6.3)。パスは書き換えず fs へ送る。 */ +#include +#include +#include +#include +#include +#include + +int pkcl_stat_convert(const fnd_stat_t *fs, struct stat *st) { + if (fs->size_hi != 0 || fs->size_lo > 0x7FFFFFFFu) { + errno = EOVERFLOW; + return -1; + } + st->st_dev = 1; + st->st_ino = 0; + st->st_mode = pkcl_mode_from_fnd(fs->type, fs->mode); + if (fs->flags & FND_STAT_READ_ONLY) + st->st_mode &= ~(mode_t)(S_IWUSR | S_IWGRP | S_IWOTH); + st->st_nlink = 1; + st->st_uid = fs->uid; + st->st_gid = 0; + st->st_size = (off_t)fs->size_lo; + st->st_mtime = (time_t)(((uint64_t)fs->modified_sec_hi << 32) | fs->modified_sec_lo); + st->st_atime = st->st_mtime; + st->st_ctime = (time_t)(((uint64_t)fs->changed_sec_hi << 32) | fs->changed_sec_lo); + return 0; +} + +int stat(const char *path, struct stat *st) { + pkcl_init(); + fnd_stat_t fs; + if (pkcl_fail(fnd_path_stat(path, &fs))) + return -1; + return pkcl_stat_convert(&fs, st); +} + +/* FS2 にシンボリックリンクはないので lstat は stat と同じ。 */ +int lstat(const char *path, struct stat *st) { return stat(path, st); } + +int access(const char *path, int mode) { + pkcl_init(); + fnd_stat_t fs; + if (pkcl_fail(fnd_path_stat(path, &fs))) + return -1; + if ((mode & W_OK) && (fs.flags & FND_STAT_READ_ONLY)) { + errno = EROFS; + return -1; + } + /* v1 のセッションは UID 0 で、R/W の検査を省略できる(FS2 §8)。実行は X bit が 1 つもなければ拒否する。 */ + if ((mode & X_OK) && fs.type == FND_TYPE_FILE && !(fs.mode & 011u)) { + errno = EACCES; + return -1; + } + return 0; +} + +int mkdir(const char *path, mode_t mode) { + pkcl_init(); + return pkcl_fail(fnd_dir_make(path, pkcl_mode_to_fnd(mode & ~pkcl_current_umask()))); +} + +int rmdir(const char *path) { + pkcl_init(); + return pkcl_fail(fnd_dir_remove(path)); +} + +int unlink(const char *path) { + pkcl_init(); + fnd_stat_t fs; + /* POSIX は unlink(ディレクトリ) を EISDIR(Linux)または EPERM で拒否する。 */ + if (fnd_path_stat(path, &fs) == FND_OK && fs.type == FND_TYPE_DIR) { + errno = EISDIR; + return -1; + } + return pkcl_fail(fnd_file_delete(path)); +} + +int rename(const char *old_path, const char *new_path) { + pkcl_init(); + /* POSIX は既存の new_path を置き換えるが、fnd_file_rename は EXISTS を返す。 + * 置換の模倣(削除してから rename)は途中で失敗すると両方を失うので、 + * 置換先が通常ファイルのときだけ行う(doc/spec_pkcl.md §6.2)。 */ + fnd_status status = fnd_file_rename(old_path, new_path); + if (status == FND_E_EXISTS) { + fnd_stat_t from, to; + if (fnd_path_stat(old_path, &from) == FND_OK && fnd_path_stat(new_path, &to) == FND_OK && + from.type == FND_TYPE_FILE && to.type == FND_TYPE_FILE) { + status = fnd_file_delete(new_path); + if (status == FND_OK) + status = fnd_file_rename(old_path, new_path); + } + } + return pkcl_fail(status); +} + +int chdir(const char *path) { + pkcl_init(); + return pkcl_fail(fnd_cwd_set(path)); +} + +char *getcwd(char *buf, size_t size) { + pkcl_init(); + uint32_t len = 0; + if (!buf || size == 0) { + errno = EINVAL; + return 0; + } + fnd_status status = fnd_cwd_get(buf, (uint32_t)size, &len); + if (status == FND_E_LIMIT) { + errno = ERANGE; + return 0; + } + return pkcl_fail(status) ? 0 : buf; +} + +int chmod(const char *path, mode_t mode) { + pkcl_init(); + uint32_t args[4] = {pkcl_mode_to_fnd(mode), 0, 0, 0}; + uint32_t n = 0; + while (path[n]) + ++n; + if (n == 0 || n > PKCL_PATH_MAX) { + errno = n ? ENAMETOOLONG : ENOENT; + return -1; + } + return pkcl_fail(fnd_call(fnd_fs(), FND_FS_CHMOD, args, path, n, 0, 0, 0)); +} + +int truncate(const char *path, off_t length) { + int fd = open(path, O_WRONLY); + if (fd < 0) + return -1; + int result = ftruncate(fd, length); + int saved = errno; + close(fd); + errno = saved; + return result; +} diff --git a/foundation/tests/pkcl/guest.c b/foundation/tests/pkcl/guest.c new file mode 100644 index 0000000..7e0f949 --- /dev/null +++ b/foundation/tests/pkcl/guest.c @@ -0,0 +1,109 @@ +/* pkcl の POSIX I/O を、実際の fs・console サービスに対して確かめるゲスト。 + * 失敗したら失敗した行番号で終了する(0 が成功)。 */ +#include +#include +#include +#include +#include +#include + +#define CHECK(x) \ + do { \ + if (!(x)) \ + _exit(__LINE__); \ + } while (0) +#define FAILS(call, code) CHECK((call) == -1 && errno == (code)) + +extern char **environ; + +int main(int argc, char **argv) { + CHECK(argc == 1 && argv[1] == 0 && environ && environ[0] == 0); + char buf[16]; + struct stat st; + + /* 標準記述子はコンソール。 */ + CHECK(isatty(0) && isatty(1) && isatty(2)); + CHECK(write(1, "pkcl: hello\n", 12) == 12); + FAILS(lseek(1, 0, SEEK_CUR), ESPIPE); + FAILS(read(1, buf, 1), EBADF); /* STDOUT は書き込み専用 */ + + /* 作成・書き込み・stat。 */ + umask(022); + int fd = open("/pkcl.txt", O_WRONLY | O_CREAT | O_EXCL, 0666); + CHECK(fd >= 3); + FAILS(open("/pkcl.txt", O_WRONLY | O_CREAT | O_EXCL, 0666), EEXIST); + CHECK(write(fd, "abcde", 5) == 5); + FAILS(read(fd, buf, 1), EBADF); + CHECK(close(fd) == 0); + FAILS(close(fd), EBADF); + CHECK(stat("/pkcl.txt", &st) == 0); + CHECK(S_ISREG(st.st_mode) && st.st_size == 5); + CHECK((st.st_mode & 0777) == 0644); /* 0666 & ~022 */ + CHECK(access("/pkcl.txt", R_OK | W_OK) == 0); + FAILS(access("/pkcl-missing", F_OK), ENOENT); + + /* シーク・読み出し・追記。 */ + fd = open("/pkcl.txt", O_RDWR); + CHECK(fd >= 3); + CHECK(lseek(fd, 2, SEEK_SET) == 2); + CHECK(read(fd, buf, 16) == 3 && memcmp(buf, "cde", 3) == 0); + CHECK(read(fd, buf, 16) == 0); + CHECK(lseek(fd, 0, SEEK_END) == 5); + CHECK(lseek(fd, -1, SEEK_CUR) == 4); + FAILS(lseek(fd, -10, SEEK_SET), EINVAL); + CHECK(close(fd) == 0); + fd = open("/pkcl.txt", O_WRONLY | O_APPEND); + CHECK(fd >= 3 && write(fd, "f", 1) == 1 && close(fd) == 0); + CHECK(stat("/pkcl.txt", &st) == 0 && st.st_size == 6); + + /* dup は読み書き位置を共有し、dup2 は指定した番号に置く。 */ + fd = open("/pkcl.txt", O_RDWR); + CHECK(fd >= 3); + int copy = dup(fd); + CHECK(copy > fd); + CHECK(lseek(fd, 1, SEEK_SET) == 1); + CHECK(read(copy, buf, 1) == 1 && buf[0] == 'b'); + CHECK(read(fd, buf, 1) == 1 && buf[0] == 'c'); + CHECK(dup2(fd, 20) == 20 && dup2(20, 20) == 20); + CHECK(read(20, buf, 1) == 1 && buf[0] == 'd'); + CHECK(fcntl(20, F_GETFD) == 0 && fcntl(20, F_SETFD, FD_CLOEXEC) == 0 && fcntl(20, F_GETFD) == FD_CLOEXEC); + CHECK((fcntl(fd, F_GETFL) & O_ACCMODE) == O_RDWR); + int high = fcntl(fd, F_DUPFD, 30); + CHECK(high == 30); + FAILS(dup2(fd, 99), EBADF); + + /* 切り詰め。 */ + CHECK(ftruncate(fd, 3) == 0); + CHECK(fstat(fd, &st) == 0 && st.st_size == 3); + CHECK(fsync(fd) == 0); + CHECK(close(fd) == 0 && close(copy) == 0 && close(20) == 0 && close(high) == 0); + CHECK(truncate("/pkcl.txt", 1) == 0); + CHECK(stat("/pkcl.txt", &st) == 0 && st.st_size == 1); + + /* ディレクトリ・rename・unlink・cwd。 */ + CHECK(mkdir("/pkcld", 0755) == 0); + FAILS(mkdir("/pkcld", 0755), EEXIST); + CHECK(stat("/pkcld", &st) == 0 && S_ISDIR(st.st_mode) && (st.st_mode & 0777) == 0755); + FAILS(unlink("/pkcld"), EISDIR); + CHECK(rename("/pkcl.txt", "/pkcld/moved.txt") == 0); + FAILS(stat("/pkcl.txt", &st), ENOENT); + FAILS(rmdir("/pkcld"), ENOTEMPTY); + CHECK(chdir("/pkcld") == 0 && getcwd(buf, sizeof buf) == buf && memcmp(buf, "/pkcld", 7) == 0); + FAILS(getcwd(buf, 3) == 0 ? -1 : 0, ERANGE); + CHECK(open("moved.txt", O_RDONLY) == 3); /* 相対パスは fs が cwd から解決する。fd 3 が空いている */ + CHECK(close(3) == 0); + CHECK(chmod("moved.txt", 0600) == 0 && stat("moved.txt", &st) == 0 && (st.st_mode & 0777) == 0600); + CHECK(rename("moved.txt", "/pkcl2.txt") == 0); /* 別ディレクトリへ */ + CHECK(chdir("/") == 0); + CHECK(rmdir("/pkcld") == 0); + /* rename は通常ファイルへの上書きもする。 */ + fd = open("/pkcl3.txt", O_WRONLY | O_CREAT, 0644); + CHECK(fd >= 3 && write(fd, "xy", 2) == 2 && close(fd) == 0); + CHECK(rename("/pkcl3.txt", "/pkcl2.txt") == 0); + CHECK(stat("/pkcl2.txt", &st) == 0 && st.st_size == 2); + CHECK(unlink("/pkcl2.txt") == 0); + FAILS(unlink("/pkcl2.txt"), ENOENT); + + CHECK(write(1, "pkcl guest passed\n", 18) == 18); + return 0; +} diff --git a/foundation/tests/pkcl/test_pkcl.py b/foundation/tests/pkcl/test_pkcl.py new file mode 100644 index 0000000..5a20b90 --- /dev/null +++ b/foundation/tests/pkcl/test_pkcl.py @@ -0,0 +1,68 @@ +"""pkcl の POSIX I/O を実際の Foundation(fs・console サービス)で確かめる。 + +foundation/boot/build.py で起動イメージを作り、pkcl を静的にリンクしたゲストを +ディスクの /bin に置いて、シェルから実行する。console のゲストテストと同じ構成。 + + python -m unittest discover -s foundation/tests/pkcl -v + +CLANG と SRC32_TESTBENCH で実行ファイルを指定できます。 +""" +import os +from pathlib import Path +import subprocess +import sys +import unittest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] +sys.path.insert(0, str(HERE.parent / 'shell')) +import test_shell # noqa: E402 + +PKCL = ROOT / 'foundation/lib/pkcl' +INCLUDES = ['-I', str(ROOT / 'foundation/include/pkcl'), '-I', str(ROOT / 'foundation/include'), + '-I', str(ROOT / 'sdk/cpt32/include')] + + +class PkclGuestTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + test_shell.ShellTests.setUpClass.__func__(cls) + guest = cls.work / 'pkclcheck.wax' + command = [sys.executable, str(ROOT / 'tools/llvm-src32/driver.py'), str(HERE / 'guest.c'), + *map(str, sorted(PKCL.glob('*.c'))), *INCLUDES, + '--entry', 'pkcl_start', '-O1', '--wax3', '--page-align', '--image-id', 'test.pkcl', + '--import-wal', str(cls.work / 'root/lib/fnd.1.wal'), + '--origin', '0x800000', '--stack-top', '0xC00000', '-o', str(guest)] + if os.environ.get('CLANG'): + command += ['--clang', os.environ['CLANG']] + result = subprocess.run(command, capture_output=True, text=True, timeout=300) + if result.returncode: + raise RuntimeError(result.stdout + result.stderr) + volume = test_shell.fs2.Volume(bytearray(cls.disk)) + volume.put('/bin/pkclcheck.wax', guest.read_bytes(), mode=0x3D) + cls.disk = bytes(volume.img) + + @classmethod + def tearDownClass(cls): + test_shell.ShellTests.tearDownClass.__func__(cls) + + def test_posix_io(self): + run = self.work / 'run' + run.mkdir() + (run / 'disk.img').write_bytes(self.disk) + (run / 'config.yaml').write_text( + 'memory:\n ram_mib: 16\n vram_mib: 4\n' + 'disk:\n - slot: 0\n type: hdd\n image: disk.img\n read_only: false\n', encoding='utf-8') + command = [str(self.harness), str(self.image), '42', '--frames', '660', '--dump-pcg', + '--cpu', os.environ.get('FOUNDATION_TEST_CPU', 'cached'), + '--type', '30=pkclcheck\\n', '--type', '230=echo status=$?\\n'] + result = subprocess.run(command, cwd=run, capture_output=True, text=True, + encoding='utf-8', errors='replace', timeout=600) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn('pkcl: hello', result.stdout) + self.assertIn('pkcl guest passed', result.stdout) + self.assertIn('status=0', result.stdout) + + +if __name__ == '__main__': + unittest.main() diff --git a/memory/2026-10-25-issue-165-pkcl.md b/memory/2026-10-25-issue-165-pkcl.md new file mode 100644 index 0000000..1b13fe2 --- /dev/null +++ b/memory/2026-10-25-issue-165-pkcl.md @@ -0,0 +1,11 @@ +# pkcl(POSIX Kernel Compatibility Layer)M0 + +- 設計は `doc/spec_pkcl.md`、実装は `foundation/lib/pkcl` と `foundation/include/pkcl`、テストは `foundation/tests/pkcl`。fdio 相当の「コンパイル時に静的リンクする POSIX→libfnd 変換層」。サービスやカーネル ABI は変えない。 +- BusyBox は GPL-2.0 なのでリポジトリに入れない(owner 指示)。外部ソースを指定してビルドする方式(Newlib と同じ)。パッチも同梱しない。 +- 1 記述子 = 1 CHANNEL。読み書き位置はサービスが持つので `dup` は位置を共有する。fd 2 は役割がないと STDOUT と同じハンドルなので、`pkcl_init` が `handle_dup` で別ハンドルにする(片方の close でもう片方が壊れるため)。 +- FS2 の mode は owner/other の 6 bit(POSIX の 0755 をそのまま保存できない)。変換は `pkcl_mode_to_fnd`/`pkcl_mode_from_fnd`。group は捨て、stat では other の複製。 +- 定数は Newlib の値(ENAMETOOLONG=91、ENOSYS=88、O_CREAT=0x200 …)。libc を Newlib にしたとき食い違わないため。 +- `off_t` は 32 bit。2 GiB 以上は `EOVERFLOW`。`fnd_seek` は libfnd にまだないので `lseek` は `ST_SEEK` を `fnd_call` で直接送る(i64 は hi, lo の順で a1, a2)。 +- fork/vfork は作らない(ENOSYS)。BusyBox は NOMMU+NOFORK で `posix_spawn` に寄せる。ギャップ(pid、環境、pipe、inode、utime、setjmp)は spec §7 と Issue #166〜#171。 +- テスト環境: Clang と llvmlite の LLVM を揃える(`pip install "llvmlite==0.46.*"` と `clang-20`、`CLANG=/usr/bin/clang-20`)。揃っていないと `ptr type is only supported in -opaque-pointers mode`。`libasound2-dev libudev-dev` で testbench をビルド(`apt-get update` が先に必要だった)。 +- 検証: `CLANG=/usr/bin/clang-20 SRC32_TESTBENCH=target/release/src32_testbench python3 -m unittest discover -s foundation/tests/pkcl -v`(約 35 秒)。 From f8784e470d0c6cfd6cff6c4fd225ee7d2dd16ecd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 09:16:13 +0000 Subject: [PATCH 2/5] wip: Newlib posix profile and pkcl Newlib adapter (#165) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FqgVXKR79fR8rybAwBZ2fY --- .../include/{pkcl => pkcl-nolibc}/errno.h | 0 .../include/{pkcl => pkcl-nolibc}/fcntl.h | 0 .../include/{pkcl => pkcl-nolibc}/sys/stat.h | 5 +- .../include/{pkcl => pkcl-nolibc}/sys/types.h | 0 .../include/{pkcl => pkcl-nolibc}/unistd.h | 3 - foundation/include/pkcl/pkcl.h | 5 + foundation/lib/pkcl/crt.c | 8 + foundation/lib/pkcl/errno.c | 4 + foundation/lib/pkcl/fd.c | 1 + foundation/lib/pkcl/fs.c | 4 + foundation/lib/pkcl/newlib/syscalls.c | 85 +++++++++++ foundation/tests/pkcl/test_pkcl.py | 2 +- tools/llvm-src32/data.py | 2 + tools/llvm-src32/driver.py | 38 ++++- tools/llvm-src32/fp.py | 2 +- tools/llvm-src32/newlib.py | 138 ++++++++++++++++-- .../runtime/newlib/include-posix/newlib.h | 21 +++ tools/llvm-src32/runtime/newlib/port.c | 3 + tools/llvm-src32/runtime/newlib/reent_alloc.c | 17 +++ 19 files changed, 316 insertions(+), 22 deletions(-) rename foundation/include/{pkcl => pkcl-nolibc}/errno.h (100%) rename foundation/include/{pkcl => pkcl-nolibc}/fcntl.h (100%) rename foundation/include/{pkcl => pkcl-nolibc}/sys/stat.h (86%) rename foundation/include/{pkcl => pkcl-nolibc}/sys/types.h (100%) rename foundation/include/{pkcl => pkcl-nolibc}/unistd.h (87%) create mode 100644 foundation/lib/pkcl/newlib/syscalls.c create mode 100644 tools/llvm-src32/runtime/newlib/include-posix/newlib.h create mode 100644 tools/llvm-src32/runtime/newlib/reent_alloc.c diff --git a/foundation/include/pkcl/errno.h b/foundation/include/pkcl-nolibc/errno.h similarity index 100% rename from foundation/include/pkcl/errno.h rename to foundation/include/pkcl-nolibc/errno.h diff --git a/foundation/include/pkcl/fcntl.h b/foundation/include/pkcl-nolibc/fcntl.h similarity index 100% rename from foundation/include/pkcl/fcntl.h rename to foundation/include/pkcl-nolibc/fcntl.h diff --git a/foundation/include/pkcl/sys/stat.h b/foundation/include/pkcl-nolibc/sys/stat.h similarity index 86% rename from foundation/include/pkcl/sys/stat.h rename to foundation/include/pkcl-nolibc/sys/stat.h index 98c163d..d77d29d 100644 --- a/foundation/include/pkcl/sys/stat.h +++ b/foundation/include/pkcl-nolibc/sys/stat.h @@ -28,7 +28,7 @@ #define S_IRWXO 0007 /* FS2 に inode 番号・リンク数・GID・atime はない。st_ino は 0、st_nlink は 1、 - * st_gid は 0、st_atime は st_mtime と同じ(doc/spec_pkcl.md §6.3)。 */ + * st_gid は 0、st_rdev は 0、st_blksize は 512、st_atime は st_mtime と同じ(doc/spec_pkcl.md §6.3)。 */ struct stat { dev_t st_dev; ino_t st_ino; @@ -36,10 +36,13 @@ struct stat { nlink_t st_nlink; uid_t st_uid; gid_t st_gid; + dev_t st_rdev; off_t st_size; time_t st_atime; time_t st_mtime; time_t st_ctime; + int32_t st_blksize; + int32_t st_blocks; }; int stat(const char *path, struct stat *st); diff --git a/foundation/include/pkcl/sys/types.h b/foundation/include/pkcl-nolibc/sys/types.h similarity index 100% rename from foundation/include/pkcl/sys/types.h rename to foundation/include/pkcl-nolibc/sys/types.h diff --git a/foundation/include/pkcl/unistd.h b/foundation/include/pkcl-nolibc/unistd.h similarity index 87% rename from foundation/include/pkcl/unistd.h rename to foundation/include/pkcl-nolibc/unistd.h index 4b4dd51..55bdfee 100644 --- a/foundation/include/pkcl/unistd.h +++ b/foundation/include/pkcl-nolibc/unistd.h @@ -15,9 +15,6 @@ #define W_OK 2 #define R_OK 4 -/* ファイル記述子は 0..PKCL_OPEN_MAX-1。カーネルのハンドル上限(63)に合わせる。 */ -#define PKCL_OPEN_MAX 64 - int close(int fd); ssize_t read(int fd, void *buf, size_t len); ssize_t write(int fd, const void *buf, size_t len); diff --git a/foundation/include/pkcl/pkcl.h b/foundation/include/pkcl/pkcl.h index a6750b4..2494e7d 100644 --- a/foundation/include/pkcl/pkcl.h +++ b/foundation/include/pkcl/pkcl.h @@ -6,6 +6,11 @@ /* pkcl の内部境界(doc/spec_pkcl.md §3)。アプリケーションは POSIX の名前だけを使い、 * これは pkcl の .c 同士と、pkcl をテストするコードのためのものです。 */ +/* ファイル記述子は 0..PKCL_OPEN_MAX-1。カーネルのハンドル上限(63)に合わせる。 */ +#ifndef PKCL_OPEN_MAX +#define PKCL_OPEN_MAX 64 +#endif + /* Foundation のエラーを errno の値に直す。0 以上(成功)は 0。 */ int pkcl_errno(fnd_status status); /* status が負なら errno を設定して -1、そうでなければ 0 を返す。 */ diff --git a/foundation/lib/pkcl/crt.c b/foundation/lib/pkcl/crt.c index 93b5eeb..3c4ca0d 100644 --- a/foundation/lib/pkcl/crt.c +++ b/foundation/lib/pkcl/crt.c @@ -1,10 +1,14 @@ /* pkcl の起動コード(doc/spec_pkcl.md §3.3)。`--entry pkcl_start` で指定する。 * 起動ブロックから argc/argv を取り出し、`int main(int, char **)` を呼んで、 * 戻り値で終了する。 */ +#define _GNU_SOURCE 1 /* libc のヘッダに O_CLOEXEC などを出させる */ #include #include extern int main(int argc, char **argv); +#ifdef PKCL_LIBC +extern void exit(int code) __attribute__((noreturn)); +#endif /* 環境変数。起動ブロックにはまだ環境がない(doc/spec_pkcl.md §7 G2)ので空から始める。 */ static char *empty_environ[1]; @@ -20,5 +24,9 @@ void pkcl_start(void) { for (uint32_t i = 0; i < argc; ++i) argv[i] = (char *)fnd_argv(i); /* 起動ブロックは書き換え可能なヒープ上にある */ argv[argc] = 0; +#ifdef PKCL_LIBC + exit(main((int)argc, argv)); /* atexit と stdio の flush を libc に任せる */ +#else fnd_exit(main((int)argc, argv)); +#endif } diff --git a/foundation/lib/pkcl/errno.c b/foundation/lib/pkcl/errno.c index 23a4458..89157a9 100644 --- a/foundation/lib/pkcl/errno.c +++ b/foundation/lib/pkcl/errno.c @@ -1,9 +1,13 @@ /* pkcl の errno と変換表(doc/spec_pkcl.md §5)。 */ +#define _GNU_SOURCE 1 /* libc のヘッダに O_CLOEXEC などを出させる */ #include #include #include +/* libc が errno をマクロにしているとき(Newlib は *__errno())は、その libc が実体を持つ。 */ +#ifndef errno int errno; +#endif int pkcl_errno(fnd_status status) { switch (status) { diff --git a/foundation/lib/pkcl/fd.c b/foundation/lib/pkcl/fd.c index 892273b..36a9fbe 100644 --- a/foundation/lib/pkcl/fd.c +++ b/foundation/lib/pkcl/fd.c @@ -1,6 +1,7 @@ /* ファイル記述子表と、ストリームに対する POSIX 呼び出し(doc/spec_pkcl.md §6.1)。 * 1 つの記述子は 1 つの Foundation ハンドル(開いたファイル 1 つ = 1 CHANNEL)を指し、 * 読み書き位置はサービス側が持つ。dup はハンドルの複製なので、位置を共有する(POSIX どおり)。 */ +#define _GNU_SOURCE 1 /* libc のヘッダに O_CLOEXEC などを出させる */ #include #include #include diff --git a/foundation/lib/pkcl/fs.c b/foundation/lib/pkcl/fs.c index 6e6a95a..fce04d8 100644 --- a/foundation/lib/pkcl/fs.c +++ b/foundation/lib/pkcl/fs.c @@ -1,4 +1,5 @@ /* パスに対する POSIX 呼び出し(doc/spec_pkcl.md §6.2〜§6.3)。パスは書き換えず fs へ送る。 */ +#define _GNU_SOURCE 1 /* libc のヘッダに O_CLOEXEC などを出させる */ #include #include #include @@ -19,7 +20,10 @@ int pkcl_stat_convert(const fnd_stat_t *fs, struct stat *st) { st->st_nlink = 1; st->st_uid = fs->uid; st->st_gid = 0; + st->st_rdev = 0; st->st_size = (off_t)fs->size_lo; + st->st_blksize = 512; + st->st_blocks = (fs->size_lo + 511u) / 512u; st->st_mtime = (time_t)(((uint64_t)fs->modified_sec_hi << 32) | fs->modified_sec_lo); st->st_atime = st->st_mtime; st->st_ctime = (time_t)(((uint64_t)fs->changed_sec_hi << 32) | fs->changed_sec_lo); diff --git a/foundation/lib/pkcl/newlib/syscalls.c b/foundation/lib/pkcl/newlib/syscalls.c new file mode 100644 index 0000000..e6df834 --- /dev/null +++ b/foundation/lib/pkcl/newlib/syscalls.c @@ -0,0 +1,85 @@ +/* Newlib の OS 層(libgloss が普通に提供する `_open` や `_sbrk` など)を、pkcl の POSIX 関数と + * Foundation のカーネルに結びつける(doc/spec_pkcl.md §9)。Newlib のヘッダを使うときだけ + * リンクする。Newlib の reent 層(`_open_r` など)は Newlib 自身が持ち、ここの `_open` を呼ぶ。 */ +#define _GNU_SOURCE 1 /* libc のヘッダに O_CLOEXEC などを出させる */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int _open(const char *path, int flags, int mode) { return open(path, flags, mode); } +int _close(int fd) { return close(fd); } +int _read(int fd, void *buf, size_t len) { return (int)read(fd, buf, len); } +int _write(int fd, const void *buf, size_t len) { return (int)write(fd, buf, len); } +off_t _lseek(int fd, off_t offset, int whence) { return lseek(fd, offset, whence); } +int _fstat(int fd, struct stat *st) { return fstat(fd, st); } +int _stat(const char *path, struct stat *st) { return stat(path, st); } +int _isatty(int fd) { return isatty(fd); } +int _unlink(const char *path) { return unlink(path); } +int _mkdir(const char *path, mode_t mode) { return mkdir(path, mode); } +int _fcntl(int fd, int cmd, int arg) { return fcntl(fd, cmd, arg); } + +/* FS2 にリンクはなく、pkcl はシグナルとプロセスの複製を持たない(§6.4、§6.5)。 */ +int _link(const char *old_path, const char *new_path) { + (void)old_path, (void)new_path; + errno = ENOSYS; + return -1; +} +int _kill(int pid, int sig) { + (void)pid, (void)sig; + errno = ENOSYS; + return -1; +} +int _fork(void) { + errno = ENOSYS; + return -1; +} +int _wait(int *status) { + (void)status; + errno = ECHILD; + return -1; +} +int _execve(const char *path, char *const argv[], char *const envp[]) { + (void)path, (void)argv, (void)envp; + errno = ENOSYS; + return -1; +} +int _getpid(void) { return 1; } /* §7 G1: 自分の pid を知る手段がまだない */ + +/* カーネルの tick(10 ms)と RTC から。 */ +int _gettimeofday(struct timeval *tv, void *tz) { + (void)tz; + if (tv) { + uint32_t ticks = (uint32_t)fnd_sys_time_get(0), hz = (uint32_t)fnd_sys_time_get(2); + tv->tv_sec = (time_t)(uint32_t)fnd_sys_time_get(3); + tv->tv_usec = hz ? (long)((ticks % hz) * (1000000u / hz)) : 0; + } + return 0; +} +clock_t _times(struct tms *buf) { + uint32_t ticks = (uint32_t)fnd_sys_time_get(0); + if (buf) + buf->tms_utime = buf->tms_stime = buf->tms_cutime = buf->tms_cstime = 0; + return (clock_t)ticks; +} + +/* ヒープは mem_grow で伸ばす(8 の倍数。縮める手段はない)。nano allocator はこれを呼ぶ。 */ +void *cpt32_newlib_sbrk(ptrdiff_t increment) { + if (increment < 0) { + errno = ENOMEM; + return (void *)-1; + } + uint32_t delta = ((uint32_t)increment + 7u) & ~7u; + int32_t old = fnd_sys_mem_grow(delta); + if (old < 0) { + errno = pkcl_errno(old); + return (void *)-1; + } + return (void *)(uintptr_t)(uint32_t)old; +} diff --git a/foundation/tests/pkcl/test_pkcl.py b/foundation/tests/pkcl/test_pkcl.py index 5a20b90..efd9b18 100644 --- a/foundation/tests/pkcl/test_pkcl.py +++ b/foundation/tests/pkcl/test_pkcl.py @@ -19,7 +19,7 @@ import test_shell # noqa: E402 PKCL = ROOT / 'foundation/lib/pkcl' -INCLUDES = ['-I', str(ROOT / 'foundation/include/pkcl'), '-I', str(ROOT / 'foundation/include'), +INCLUDES = ['-I', str(ROOT / 'foundation/include/pkcl-nolibc'), '-I', str(ROOT / 'foundation/include'), '-I', str(ROOT / 'sdk/cpt32/include')] diff --git a/tools/llvm-src32/data.py b/tools/llvm-src32/data.py index 1850a08..0c6e329 100644 --- a/tools/llvm-src32/data.py +++ b/tools/llvm-src32/data.py @@ -282,6 +282,8 @@ def _read_global(self, value): alignment = max(alignment, explicit) elif attribute.startswith("!"): continue # Debug/optimization metadata is not stored in the image. + elif re.fullmatch(r'section\s+"\.(s?data|s?bss|rodata[A-Za-z0-9_.]*)"', attribute): + continue # Placement is by the image layout; Newlib puts _impure_ptr in .sdata on MIPS. else: raise CompileError(f"unsupported global attribute: {attribute}") if value.is_declaration: diff --git a/tools/llvm-src32/driver.py b/tools/llvm-src32/driver.py index 79be7b6..f776211 100644 --- a/tools/llvm-src32/driver.py +++ b/tools/llvm-src32/driver.py @@ -89,6 +89,11 @@ def parser() -> argparse.ArgumentParser: help="parallel Clang invocations (default: CPU count; output does not depend on N)") p.add_argument("--newlib-source", type=Path, help="external Newlib source root (optional portable integer/string/allocator profile)") p.add_argument("--newlib-heap-size", type=address, default=65536, help="reserved Newlib heap bytes (default 65536; multiple of 8)") + p.add_argument("--newlib-profile", choices=("portable", "posix"), default="portable", + help="portable: strings, integers, sort and a bounded allocator; posix: most of libc " + "(stdio, stdlib, time, locale…), leaving errno, sbrk and the system calls to the OS layer") + p.add_argument("--newlib-cache", type=Path, help="directory for the compiled Newlib IR of the posix profile " + "(default: $SRC32_CACHE or ~/.cache/llvm-src32)") return p @@ -117,12 +122,21 @@ def compile_c(clang: str, source: Path, output: Path, args, defines=(), runtime= if runtime and (source == ROOT / "runtime" / "string.c" or softfloat): optimization = "-O2" cxx = source.suffix.lower() in CXX_SUFFIXES + newlib = getattr(args, "newlib", None) + posix_library = bool(runtime and newlib and newlib.posix) + if posix_library: + # Newlib is written for GNU C and builds with the old implicit-declaration rules. + defines = (*defines, *newlib_posix_defines()) command = [clang, "--target=mips-unknown-none", "-mabi=o32", "-msoft-float", "-ffreestanding", "-fno-builtin", "-ffp-contract=off", "-fno-stack-protector", "-fno-pic", "-fno-pie", - "-fno-vectorize", "-fno-slp-vectorize", "-fsigned-char", "-nostdinc", - "-std=c++17" if cxx else "-std=c11", + "-fno-vectorize", "-fno-slp-vectorize", "-fsigned-char", "-G0", "-nostdinc", + "-std=c++17" if cxx else "-std=gnu11" if posix_library else "-std=c11", optimization, "-S", "-emit-llvm"] - newlib = getattr(args, "newlib", None) + if posix_library: + if source.is_relative_to(newlib.root / "newlib" / "libm"): + command.extend(["-I", str(newlib.root / "newlib" / "libm" / "common")]) + command.extend(["-Wno-error=implicit-function-declaration", "-Wno-error=int-conversion", + "-Wno-error=incompatible-pointer-types", "-Wno-error=implicit-int", "-w"]) for include in newlib.headers() if newlib else [ROOT / "runtime" / "include"]: command.extend(["-isystem", str(include)]) if newlib: @@ -153,6 +167,11 @@ def compile_c(clang: str, source: Path, output: Path, args, defines=(), runtime= raise DriverError(f"Clang failed for {source}:\n{result.stderr.strip() or result.stdout.strip()}") +def newlib_posix_defines(): + from newlib import POSIX_DEFINES + return POSIX_DEFINES + + def load_python(path: Path, name: str): spec = importlib.util.spec_from_file_location(name, path) if spec is None or spec.loader is None: @@ -304,7 +323,9 @@ def check_link_layout(reference, candidate, source): if args.newlib_source: from newlib import Newlib clang = clang or find_clang(args.clang) - args.newlib = Newlib(args.newlib_source, clang, args.newlib_heap_size) + args.newlib = Newlib(args.newlib_source, clang, args.newlib_heap_size, profile=args.newlib_profile, + cache=args.newlib_cache, jobs=args.jobs if args.jobs > 0 else (os.cpu_count() or 1)) + args.newlib.compile_ir = lambda source, ir: compile_c(clang, source, ir, args, runtime=True) dependencies.add(args.newlib.root / "COPYING.NEWLIB") jobs = args.jobs if args.jobs > 0 else (os.cpu_count() or 1) args.jobs = jobs @@ -342,13 +363,18 @@ def compile_input(item): if args.newlib: while True: defined = {f.name for f in linked.functions if not f.is_declaration} - missing = {f.name for f in linked.functions if f.is_declaration} - defined + defined |= {g.name for g in linked.global_variables if not g.is_declaration} + missing = {f.name for f in linked.functions if f.is_declaration} + missing |= {g.name for g in linked.global_variables if g.is_declaration} + missing -= defined pending = args.newlib.pending(missing, defined) if not pending: break def build_newlib(item): - name, source, defines = item + name, source, defines, *cached = item + if cached: # the posix profile compiled it into its IR cache already + return cached[0], cached[0], None ir = work / f"newlib-{name}.ll" dep = ir.with_suffix(".d") if args.depfile else None # Do not leak application defines/includes into upstream libc. diff --git a/tools/llvm-src32/fp.py b/tools/llvm-src32/fp.py index 66a1984..32e2914 100644 --- a/tools/llvm-src32/fp.py +++ b/tools/llvm-src32/fp.py @@ -149,7 +149,7 @@ def literal_bits(ty, text): """Bit pattern of the LLVM constant `text` (without type) of float/double.""" text = text.strip() double = kind(ty) == "df" - if re.fullmatch(r"0x[0-9a-fA-F]{16}", text): + if re.fullmatch(r"0x[0-9a-fA-F]{1,16}", text): bits = int(text, 16) # LLVM prints every float constant as a double if double: return bits diff --git a/tools/llvm-src32/newlib.py b/tools/llvm-src32/newlib.py index c5c64e9..5963cde 100644 --- a/tools/llvm-src32/newlib.py +++ b/tools/llvm-src32/newlib.py @@ -1,4 +1,8 @@ """Optional source-only Newlib integration. Never downloads or vendors Newlib.""" +from concurrent.futures import ThreadPoolExecutor +import hashlib +import json +import os from pathlib import Path import subprocess @@ -8,11 +12,29 @@ SOURCES = {name: f"string/{name}.c" for name in STRINGS} SOURCES.update({name: f"stdlib/{name}.c" for name in INTEGERS}) SOURCES.update({name: f"search/{name}.c" for name in ("qsort", "bsearch")}) +# The "posix" profile (doc/spec_pkcl.md §9): most of Newlib's libc, compiled on demand +# from a symbol index. nano-* are the size-reduced variants of printf, which +# the full ones replace; posix/, sys/, machine/ and friends are OS or CPU glue +# that pkcl (foundation/lib/pkcl) provides instead. +POSIX_DIRS = ("libc/argz", "libc/ctype", "libc/errno", "libc/locale", "libc/misc", "libc/reent", + "libc/search", "libc/signal", "libc/stdio", "libc/stdlib", "libc/string", "libc/time", + "libm/common", "libm/math") +POSIX_EXCLUDE = ("nano-", "mallocr", "malloc.c", "mlock", "nano-mallocr", "sbrkr", "mstats", "malloc_r", + "/stdlib/cxa_", "/stdlib/callocr.c", "/stdlib/cfreer.c", "/stdlib/freer.c", "/stdlib/malignr.c", + "/stdlib/mallinfor.c", "/stdlib/malloptr.c", "/stdlib/mallstatsr.c", "/stdlib/msizer.c", + "/stdlib/pvallocr.c", "/stdlib/reallocr.c", "/stdlib/vallocr.c", "/stdlib/mallocr.c", "/misc/ffs", "fdopen64", "/stdio64/", + # the driver's own string.c supplies these (it compiles them at -O2) + "/string/memcpy.c", "/string/memmove.c", "/string/memset.c", "/string/memcmp.c", "/string/strlen.c") +# Files that Newlib compiles as variants of another file; compile only the first of a group. +POSIX_PRIORITY = ("stdio/vfprintf.c", "stdio/vfscanf.c", "stdio/svfprintf.c", "stdio/svfscanf.c") +POSIX_DEFINES = ("_COMPILING_NEWLIB", "_GNU_SOURCE") +CACHE_VERSION = "1" +REENT_ALLOCATORS = ("_malloc_r", "_free_r", "_realloc_r", "_calloc_r", "__malloc_lock", "__malloc_unlock") ALLOCATORS = ("malloc", "free", "calloc", "realloc", "malloc_usable_size") class Newlib: - def __init__(self, source, clang, heap_size): + def __init__(self, source, clang, heap_size, profile="portable", compile_ir=None, jobs=1, cache=None): self.root = Path(source).resolve() self.libc = self.root / "newlib" / "libc" required = [self.libc / "include" / "newlib.h", self.root / "COPYING.NEWLIB", @@ -30,18 +52,104 @@ def __init__(self, source, clang, heap_size): raise ValueError(f"Clang resource headers not found: {self.resource}") self.heap_size = heap_size self.loaded = set() + self.profile = profile + self.posix = profile == "posix" + self.compile_ir = compile_ir + self.jobs = jobs + self.cache_root = cache + self.index = None + self.clang = clang def headers(self): # Port configuration, upstream declarations, then compiler-owned headers. - return [PORT / "include", self.libc / "include", self.resource] + return [PORT / ("include-posix" if self.posix else "include"), self.libc / "include", self.resource] - def pending(self, missing, defined=()): + # ---- posix profile: a cache of the IR of every library file, and the symbol index ---- + + def candidates(self): + files = [] + for directory in POSIX_DIRS: + for path in sorted((self.libc.parent / directory).glob("*.c")): + relative = path.relative_to(self.libc.parent).as_posix() + if any(mark in "/" + relative or path.name.startswith(mark) for mark in POSIX_EXCLUDE): + continue + files.append(path) + return files + + def cache_dir(self): + version = subprocess.run([self.clang, "--version"], capture_output=True, text=True).stdout + digest = hashlib.sha256() + for part in (CACHE_VERSION, str(self.root), version, self.profile, repr(POSIX_DIRS), repr(POSIX_EXCLUDE), + repr(POSIX_DEFINES)): + digest.update(part.encode()) + for path in sorted((PORT / "include-posix").glob("*.h")): + digest.update(path.read_bytes()) + base = Path(self.cache_root) if self.cache_root else Path( + os.environ.get("SRC32_CACHE") or Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "llvm-src32") + return base / f"newlib-{digest.hexdigest()[:16]}" + + def build_index(self): + """Compile every candidate to IR once (cached) and map defined symbols to their IR file.""" + from llvmlite import binding as llvm + directory = self.cache_dir() + index_file = directory / "index.json" + if index_file.is_file(): + self.index = json.loads(index_file.read_text(encoding="utf-8")) + return + directory.mkdir(parents=True, exist_ok=True) + files = self.candidates() + + def build(path): + ir = directory / (path.relative_to(self.libc.parent).as_posix().replace("/", "-")[:-2] + ".ll") + if not ir.is_file(): + try: + self.compile_ir(path, ir) + except Exception as exc: # a file the toolchain cannot compile just provides nothing + return path, None, str(exc).splitlines()[0] + return path, ir, None + + with ThreadPoolExecutor(max_workers=self.jobs) as pool: + built = list(pool.map(build, files)) + symbols, failed = {}, {} + for path, ir, error in built: + relative = path.relative_to(self.libc.parent).as_posix() + if ir is None: + failed[relative] = error + continue + module = llvm.parse_assembly(ir.read_text(encoding="utf-8")) + names = [f.name for f in module.functions if not f.is_declaration and str(f.linkage) not in + ("Linkage.internal", "Linkage.private")] + names += [g.name for g in module.global_variables if not g.is_declaration and str(g.linkage) not in + ("Linkage.internal", "Linkage.private")] + for name in names: + entry = symbols.setdefault(name, []) + entry.append(ir.name) + for name, entry in symbols.items(): + entry.sort(key=lambda item: (0 if any(item.endswith(p.replace("/", "-")[:-2] + ".ll") + for p in POSIX_PRIORITY) else 1, item)) + data = {"symbols": {k: v[0] for k, v in symbols.items()}, "failed": failed} + index_file.write_text(json.dumps(data, indent=0, sort_keys=True), encoding="utf-8") + self.index = data + + def posix_pending(self, missing, defined): + if self.index is None: + self.build_index() + directory = self.cache_dir() result = [] for name in sorted(missing): - if name in SOURCES and name not in self.loaded: + ir = self.index["symbols"].get(name) + if ir and ir not in self.loaded: + self.loaded.add(ir) + result.append((ir[:-3], None, [], directory / ir)) + return result + + def pending(self, missing, defined=()): + result = self.posix_pending(missing, defined) if self.posix else [] + for name in sorted(missing): + if not self.posix and name in SOURCES and name not in self.loaded: self.loaded.add(name) result.append((name, self.libc / SOURCES[name], [])) - if set(ALLOCATORS) & missing and "allocator" not in self.loaded: + if (set(ALLOCATORS) | set(REENT_ALLOCATORS)) & missing and "allocator" not in self.loaded: if set(ALLOCATORS) & set(defined): raise ValueError("Newlib nano allocator cannot be mixed with user-defined malloc/free/realloc/calloc/malloc_usable_size") self.loaded.add("allocator") @@ -49,9 +157,19 @@ def pending(self, missing, defined=()): result.append(("allocator", self.libc / "stdlib/nano-mallocr.c", ["DEFINE_MALLOC", "DEFINE_FREE", "DEFINE_CALLOC", "DEFINE_REALLOC", "DEFINE_MALLOC_USABLE_SIZE"])) - if "__errno" in missing or "cpt32_newlib_sbrk" in missing: - if "port" not in self.loaded: - self.loaded.add("port") - result.append(("port", PORT / "port.c", - [f"CPT32_NEWLIB_HEAP_SIZE={self.heap_size}"])) + return result + self.port_pending(missing) + + def port_pending(self, missing): + # errno and a bounded heap, unless the program (pkcl on Foundation) brings its own. + result = [] + # (the posix profile has Newlib's own __errno, which keeps errno in the reent structure) + wanted = {"cpt32_newlib_sbrk"} if self.posix else {"__errno", "cpt32_newlib_sbrk"} + if wanted & missing and "port" not in self.loaded: + self.loaded.add("port") + defines = [f"CPT32_NEWLIB_HEAP_SIZE={self.heap_size}"] + (["CPT32_NEWLIB_NO_ERRNO"] if self.posix else []) + result.append(("port", PORT / "port.c", defines)) + # Newlib's own code calls the reentrant forms of the allocator. + if self.posix and missing & set(REENT_ALLOCATORS) and "reent-alloc" not in self.loaded: + self.loaded.add("reent-alloc") + result.append(("reent-alloc", PORT / "reent_alloc.c", [])) return result diff --git a/tools/llvm-src32/runtime/newlib/include-posix/newlib.h b/tools/llvm-src32/runtime/newlib/include-posix/newlib.h new file mode 100644 index 0000000..7a96e34 --- /dev/null +++ b/tools/llvm-src32/runtime/newlib/include-posix/newlib.h @@ -0,0 +1,21 @@ +/* CPT32 configuration for the Newlib "posix" profile (doc/spec_pkcl.md §9). + * Newlib normally generates this file with configure; the values here are the + * ones a single-threaded, 32-bit big-endian target without a hardware FPU + * (double == long double) needs. */ +#ifndef __NEWLIB_H__ +#define __NEWLIB_H__ 1 +#include <_newlib_version.h> +#define __SINGLE_THREAD__ 1 +#define _WANT_USE_LONG_TIME_T 1 +#define _LDBL_EQ_DBL 1 +#define _ATEXIT_DYNAMIC_ALLOC 1 +#define _FVWRITE_IN_STREAMIO 1 +#define _FSEEK_OPTIMIZATION 1 +#define _UNBUF_STREAM_OPT 1 +#define _WANT_IO_C99_FORMATS 1 +#define _WANT_IO_LONG_LONG 1 +#define _WANT_USE_GDTOA 1 +#define _MB_CAPABLE 1 +#define _MB_LEN_MAX 8 +#define _REENT_CHECK_VERIFY 1 +#endif diff --git a/tools/llvm-src32/runtime/newlib/port.c b/tools/llvm-src32/runtime/newlib/port.c index 5be7650..dec845d 100644 --- a/tools/llvm-src32/runtime/newlib/port.c +++ b/tools/llvm-src32/runtime/newlib/port.c @@ -5,10 +5,13 @@ static _Alignas(8) unsigned char heap[CPT32_NEWLIB_HEAP_SIZE]; static size_t used; + +#ifndef CPT32_NEWLIB_NO_ERRNO static int error_number; /* 単一実行コンテキストの errno の格納先を返す。 */ int *__errno(void) { return &error_number; } +#endif /* 静的に予約したヒープ内で break を移動し、範囲外では ENOMEM を返す。 */ void *cpt32_newlib_sbrk(ptrdiff_t increment) diff --git a/tools/llvm-src32/runtime/newlib/reent_alloc.c b/tools/llvm-src32/runtime/newlib/reent_alloc.c new file mode 100644 index 0000000..9623e6d --- /dev/null +++ b/tools/llvm-src32/runtime/newlib/reent_alloc.c @@ -0,0 +1,17 @@ +/* The reentrant allocator entry points Newlib's own code calls (the posix + * profile), on top of the standalone nano allocator. Single context: no locks. */ +#include + +void *malloc(size_t size); +void free(void *p); +void *realloc(void *p, size_t size); +void *calloc(size_t count, size_t size); + +struct _reent; + +void *_malloc_r(struct _reent *reent, size_t size) { (void)reent; return malloc(size); } +void _free_r(struct _reent *reent, void *p) { (void)reent; free(p); } +void *_realloc_r(struct _reent *reent, void *p, size_t size) { (void)reent; return realloc(p, size); } +void *_calloc_r(struct _reent *reent, size_t count, size_t size) { (void)reent; return calloc(count, size); } +void __malloc_lock(struct _reent *reent) { (void)reent; } +void __malloc_unlock(struct _reent *reent) { (void)reent; } From 34825e90de89a9787a755b8ed8b1f2a73b1ef02d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 09:24:05 +0000 Subject: [PATCH 3/5] =?UTF-8?q?pkcl:=20Newlib=20posix=20profile=E3=80=81PO?= =?UTF-8?q?SIX=20=E3=83=98=E3=83=83=E3=83=80=E3=80=81BusyBox=20=E3=83=93?= =?UTF-8?q?=E3=83=AB=E3=83=89=E3=82=B9=E3=82=AF=E3=83=AA=E3=83=97=E3=83=88?= =?UTF-8?q?(=E3=82=B3=E3=83=B3=E3=83=91=E3=82=A4=E3=83=AB=20140/160)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FqgVXKR79fR8rybAwBZ2fY --- foundation/include/pkcl-posix/arpa/inet.h | 10 + foundation/include/pkcl-posix/byteswap.h | 9 + foundation/include/pkcl-posix/dirent.h | 32 +++ foundation/include/pkcl-posix/endian.h | 12 ++ foundation/include/pkcl-posix/features.h | 1 + foundation/include/pkcl-posix/mntent.h | 19 ++ foundation/include/pkcl-posix/net/if.h | 4 + foundation/include/pkcl-posix/netdb.h | 41 ++++ foundation/include/pkcl-posix/netinet/in.h | 45 ++++ foundation/include/pkcl-posix/poll.h | 21 ++ foundation/include/pkcl-posix/stdlib.h | 7 + foundation/include/pkcl-posix/sys/ioctl.h | 15 ++ foundation/include/pkcl-posix/sys/mman.h | 20 ++ foundation/include/pkcl-posix/sys/poll.h | 1 + foundation/include/pkcl-posix/sys/socket.h | 62 ++++++ foundation/include/pkcl-posix/sys/statfs.h | 24 +++ foundation/include/pkcl-posix/sys/statvfs.h | 27 +++ foundation/include/pkcl-posix/sys/sysmacros.h | 8 + foundation/include/pkcl-posix/sys/termios.h | 90 ++++++++ foundation/include/pkcl-posix/sys/un.h | 10 + foundation/include/pkcl-posix/sys/utsname.h | 15 ++ foundation/include/pkcl-posix/sys/vfs.h | 1 + foundation/include/pkcl-posix/syslog.h | 30 +++ tools/llvm-src32/driver.py | 30 ++- tools/pkcl/busybox.config | 28 +++ tools/pkcl/busybox.py | 203 ++++++++++++++++++ 26 files changed, 764 insertions(+), 1 deletion(-) create mode 100644 foundation/include/pkcl-posix/arpa/inet.h create mode 100644 foundation/include/pkcl-posix/byteswap.h create mode 100644 foundation/include/pkcl-posix/dirent.h create mode 100644 foundation/include/pkcl-posix/endian.h create mode 100644 foundation/include/pkcl-posix/features.h create mode 100644 foundation/include/pkcl-posix/mntent.h create mode 100644 foundation/include/pkcl-posix/net/if.h create mode 100644 foundation/include/pkcl-posix/netdb.h create mode 100644 foundation/include/pkcl-posix/netinet/in.h create mode 100644 foundation/include/pkcl-posix/poll.h create mode 100644 foundation/include/pkcl-posix/stdlib.h create mode 100644 foundation/include/pkcl-posix/sys/ioctl.h create mode 100644 foundation/include/pkcl-posix/sys/mman.h create mode 100644 foundation/include/pkcl-posix/sys/poll.h create mode 100644 foundation/include/pkcl-posix/sys/socket.h create mode 100644 foundation/include/pkcl-posix/sys/statfs.h create mode 100644 foundation/include/pkcl-posix/sys/statvfs.h create mode 100644 foundation/include/pkcl-posix/sys/sysmacros.h create mode 100644 foundation/include/pkcl-posix/sys/termios.h create mode 100644 foundation/include/pkcl-posix/sys/un.h create mode 100644 foundation/include/pkcl-posix/sys/utsname.h create mode 100644 foundation/include/pkcl-posix/sys/vfs.h create mode 100644 foundation/include/pkcl-posix/syslog.h create mode 100644 tools/pkcl/busybox.config create mode 100644 tools/pkcl/busybox.py diff --git a/foundation/include/pkcl-posix/arpa/inet.h b/foundation/include/pkcl-posix/arpa/inet.h new file mode 100644 index 0000000..e6adc94 --- /dev/null +++ b/foundation/include/pkcl-posix/arpa/inet.h @@ -0,0 +1,10 @@ +#ifndef PKCL_ARPA_INET_H +#define PKCL_ARPA_INET_H +#include + +int inet_aton(const char *text, struct in_addr *addr); +char *inet_ntoa(struct in_addr addr); +int inet_pton(int family, const char *text, void *dst); +const char *inet_ntop(int family, const void *src, char *dst, socklen_t size); + +#endif diff --git a/foundation/include/pkcl-posix/byteswap.h b/foundation/include/pkcl-posix/byteswap.h new file mode 100644 index 0000000..3d5ab28 --- /dev/null +++ b/foundation/include/pkcl-posix/byteswap.h @@ -0,0 +1,9 @@ +#ifndef PKCL_BYTESWAP_H +#define PKCL_BYTESWAP_H +#include + +#define bswap_16(x) ((uint16_t)__builtin_bswap16((uint16_t)(x))) +#define bswap_32(x) ((uint32_t)__builtin_bswap32((uint32_t)(x))) +#define bswap_64(x) ((uint64_t)__builtin_bswap64((uint64_t)(x))) + +#endif diff --git a/foundation/include/pkcl-posix/dirent.h b/foundation/include/pkcl-posix/dirent.h new file mode 100644 index 0000000..3de4c9b --- /dev/null +++ b/foundation/include/pkcl-posix/dirent.h @@ -0,0 +1,32 @@ +#ifndef PKCL_DIRENT_H +#define PKCL_DIRENT_H +#include + +/* Directory streams over fnd_dir_open / fnd_dir_next (doc/spec_pkcl.md §6.2). + * Newlib's own is an #error on a target without one. */ +#define DT_UNKNOWN 0 +#define DT_CHR 2 +#define DT_DIR 4 +#define DT_REG 8 + +#define NAME_MAX 1024 +#ifndef MAXNAMLEN +#define MAXNAMLEN NAME_MAX +#endif + +struct dirent { + ino_t d_ino; + unsigned char d_type; + char d_name[NAME_MAX + 1]; +}; + +typedef struct pkcl_dir DIR; + +DIR *opendir(const char *path); +struct dirent *readdir(DIR *dir); +int readdir_r(DIR *dir, struct dirent *entry, struct dirent **result); +void rewinddir(DIR *dir); +int closedir(DIR *dir); +int dirfd(DIR *dir); + +#endif diff --git a/foundation/include/pkcl-posix/endian.h b/foundation/include/pkcl-posix/endian.h new file mode 100644 index 0000000..b447d58 --- /dev/null +++ b/foundation/include/pkcl-posix/endian.h @@ -0,0 +1,12 @@ +#ifndef PKCL_ENDIAN_H +#define PKCL_ENDIAN_H +#include + +/* Newlib spells these with one underscore; glibc-style code (BusyBox) tests the double one. */ +#ifndef __BYTE_ORDER +#define __BYTE_ORDER _BYTE_ORDER +#define __BIG_ENDIAN _BIG_ENDIAN +#define __LITTLE_ENDIAN _LITTLE_ENDIAN +#endif + +#endif diff --git a/foundation/include/pkcl-posix/features.h b/foundation/include/pkcl-posix/features.h new file mode 100644 index 0000000..c36357d --- /dev/null +++ b/foundation/include/pkcl-posix/features.h @@ -0,0 +1 @@ +#include diff --git a/foundation/include/pkcl-posix/mntent.h b/foundation/include/pkcl-posix/mntent.h new file mode 100644 index 0000000..8009265 --- /dev/null +++ b/foundation/include/pkcl-posix/mntent.h @@ -0,0 +1,19 @@ +#ifndef PKCL_MNTENT_H +#define PKCL_MNTENT_H +#include + +/* Foundation has no mount table to read (doc/spec_pkcl.md §6.8). */ +struct mntent { + char *mnt_fsname; + char *mnt_dir; + char *mnt_type; + char *mnt_opts; + int mnt_freq; + int mnt_passno; +}; + +FILE *setmntent(const char *file, const char *mode); +struct mntent *getmntent(FILE *stream); +int endmntent(FILE *stream); + +#endif diff --git a/foundation/include/pkcl-posix/net/if.h b/foundation/include/pkcl-posix/net/if.h new file mode 100644 index 0000000..c865e72 --- /dev/null +++ b/foundation/include/pkcl-posix/net/if.h @@ -0,0 +1,4 @@ +#ifndef PKCL_NET_IF_H +#define PKCL_NET_IF_H +#define IF_NAMESIZE 16 +#endif diff --git a/foundation/include/pkcl-posix/netdb.h b/foundation/include/pkcl-posix/netdb.h new file mode 100644 index 0000000..aab6c93 --- /dev/null +++ b/foundation/include/pkcl-posix/netdb.h @@ -0,0 +1,41 @@ +#ifndef PKCL_NETDB_H +#define PKCL_NETDB_H +#include + +/* No resolver (doc/spec_pkcl.md §6.8): declarations only. */ +struct hostent { + char *h_name; + char **h_aliases; + int h_addrtype; + int h_length; + char **h_addr_list; +}; + +struct addrinfo { + int ai_flags; + int ai_family; + int ai_socktype; + int ai_protocol; + socklen_t ai_addrlen; + struct sockaddr *ai_addr; + char *ai_canonname; + struct addrinfo *ai_next; +}; + +#define AI_PASSIVE 1 +#define AI_CANONNAME 2 +#define AI_NUMERICHOST 4 +#define NI_NUMERICHOST 1 +#define NI_NUMERICSERV 2 +#define EAI_NONAME (-2) +#define EAI_FAIL (-4) +#define EAI_SYSTEM (-11) + +struct hostent *gethostbyname(const char *name); +int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); +void freeaddrinfo(struct addrinfo *res); +const char *gai_strerror(int code); +int getnameinfo(const struct sockaddr *addr, socklen_t len, char *host, socklen_t hostlen, + char *service, socklen_t servicelen, int flags); + +#endif diff --git a/foundation/include/pkcl-posix/netinet/in.h b/foundation/include/pkcl-posix/netinet/in.h new file mode 100644 index 0000000..594c29d --- /dev/null +++ b/foundation/include/pkcl-posix/netinet/in.h @@ -0,0 +1,45 @@ +#ifndef PKCL_NETINET_IN_H +#define PKCL_NETINET_IN_H +#include +#include + +typedef uint16_t in_port_t; +typedef uint32_t in_addr_t; + +struct in_addr { + in_addr_t s_addr; +}; + +struct in6_addr { + uint8_t s6_addr[16]; +}; + +struct sockaddr_in { + sa_family_t sin_family; + in_port_t sin_port; + struct in_addr sin_addr; + unsigned char sin_zero[8]; +}; + +struct sockaddr_in6 { + sa_family_t sin6_family; + in_port_t sin6_port; + uint32_t sin6_flowinfo; + struct in6_addr sin6_addr; + uint32_t sin6_scope_id; +}; + +#define INADDR_ANY ((in_addr_t)0) +#define INADDR_LOOPBACK ((in_addr_t)0x7f000001) +#define IPPROTO_TCP 6 +#define IPPROTO_UDP 17 +#define INET_ADDRSTRLEN 16 +#define INET6_ADDRSTRLEN 46 + +/* SRC32 is big endian: network order is host order. */ +#define htons(x) ((uint16_t)(x)) +#define ntohs(x) ((uint16_t)(x)) +#define htonl(x) ((uint32_t)(x)) +#define ntohl(x) ((uint32_t)(x)) + +#endif diff --git a/foundation/include/pkcl-posix/poll.h b/foundation/include/pkcl-posix/poll.h new file mode 100644 index 0000000..7903e46 --- /dev/null +++ b/foundation/include/pkcl-posix/poll.h @@ -0,0 +1,21 @@ +#ifndef PKCL_POLL_H +#define PKCL_POLL_H + +#define POLLIN 0x001 +#define POLLPRI 0x002 +#define POLLOUT 0x004 +#define POLLERR 0x008 +#define POLLHUP 0x010 +#define POLLNVAL 0x020 + +typedef unsigned int nfds_t; + +struct pollfd { + int fd; + short events; + short revents; +}; + +int poll(struct pollfd *fds, nfds_t nfds, int timeout_ms); + +#endif diff --git a/foundation/include/pkcl-posix/stdlib.h b/foundation/include/pkcl-posix/stdlib.h new file mode 100644 index 0000000..b46f0c9 --- /dev/null +++ b/foundation/include/pkcl-posix/stdlib.h @@ -0,0 +1,7 @@ +/* Newlib's declares itoa and utoa (BSD), which BusyBox defines itself with another + * prototype. Hide Newlib's two declarations from code that includes this header. */ +#define itoa __newlib_itoa +#define utoa __newlib_utoa +#include_next +#undef itoa +#undef utoa diff --git a/foundation/include/pkcl-posix/sys/ioctl.h b/foundation/include/pkcl-posix/sys/ioctl.h new file mode 100644 index 0000000..2f8396b --- /dev/null +++ b/foundation/include/pkcl-posix/sys/ioctl.h @@ -0,0 +1,15 @@ +#ifndef PKCL_SYS_IOCTL_H +#define PKCL_SYS_IOCTL_H + +/* Only the terminal requests pkcl translates (doc/spec_pkcl.md §6.6). */ +#define TIOCGWINSZ 0x5413 +#define TIOCSWINSZ 0x5414 +#define FIONREAD 0x541B + +struct winsize { + unsigned short ws_row, ws_col, ws_xpixel, ws_ypixel; +}; + +int ioctl(int fd, unsigned long request, ...); + +#endif diff --git a/foundation/include/pkcl-posix/sys/mman.h b/foundation/include/pkcl-posix/sys/mman.h new file mode 100644 index 0000000..0669ebc --- /dev/null +++ b/foundation/include/pkcl-posix/sys/mman.h @@ -0,0 +1,20 @@ +#ifndef PKCL_SYS_MMAN_H +#define PKCL_SYS_MMAN_H +#include + +/* Foundation has no memory mapping (doc/spec_pkcl.md §6.1): mmap fails with ENOSYS. */ +#define PROT_NONE 0 +#define PROT_READ 1 +#define PROT_WRITE 2 +#define PROT_EXEC 4 +#define MAP_SHARED 1 +#define MAP_PRIVATE 2 +#define MAP_ANONYMOUS 0x20 +#define MAP_ANON MAP_ANONYMOUS +#define MAP_FIXED 0x10 +#define MAP_FAILED ((void *)-1) + +void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset); +int munmap(void *addr, size_t len); + +#endif diff --git a/foundation/include/pkcl-posix/sys/poll.h b/foundation/include/pkcl-posix/sys/poll.h new file mode 100644 index 0000000..779ec77 --- /dev/null +++ b/foundation/include/pkcl-posix/sys/poll.h @@ -0,0 +1 @@ +#include diff --git a/foundation/include/pkcl-posix/sys/socket.h b/foundation/include/pkcl-posix/sys/socket.h new file mode 100644 index 0000000..d628cd9 --- /dev/null +++ b/foundation/include/pkcl-posix/sys/socket.h @@ -0,0 +1,62 @@ +#ifndef PKCL_SYS_SOCKET_H +#define PKCL_SYS_SOCKET_H +#include + +/* Foundation has no network stack (doc/spec_pkcl.md §6.8). These declarations let code + * that mentions sockets compile; every call fails with ENOSYS. */ +typedef unsigned int socklen_t; +typedef unsigned short sa_family_t; + +#define AF_UNSPEC 0 +#define AF_UNIX 1 +#define AF_INET 2 +#define AF_INET6 10 +#define AF_LOCAL AF_UNIX +#define PF_UNSPEC AF_UNSPEC +#define PF_UNIX AF_UNIX +#define PF_INET AF_INET +#define PF_INET6 AF_INET6 +#define SOCK_STREAM 1 +#define SOCK_DGRAM 2 +#define SOCK_RAW 3 +#define SOCK_RDM 4 +#define SOCK_SEQPACKET 5 +#define SHUT_RD 0 +#define SHUT_WR 1 +#define SHUT_RDWR 2 +#define MSG_PEEK 2 +#define MSG_DONTWAIT 0x40 +#define MSG_NOSIGNAL 0x4000 +#define SO_ERROR 4 +#define SO_BROADCAST 6 +#define SO_KEEPALIVE 9 +#define SO_RCVBUF 8 +#define SO_SNDBUF 7 +#define SOL_SOCKET 1 +#define SO_REUSEADDR 2 +#define SOMAXCONN 128 + +struct sockaddr { + sa_family_t sa_family; + char sa_data[14]; +}; + +struct sockaddr_storage { + sa_family_t ss_family; + char ss_pad[126]; +}; + +int shutdown(int fd, int how); +int socket(int domain, int type, int protocol); +int connect(int fd, const struct sockaddr *addr, socklen_t len); +int bind(int fd, const struct sockaddr *addr, socklen_t len); +int listen(int fd, int backlog); +int accept(int fd, struct sockaddr *addr, socklen_t *len); +int setsockopt(int fd, int level, int name, const void *value, socklen_t len); +int getsockopt(int fd, int level, int name, void *value, socklen_t *len); +int getsockname(int fd, struct sockaddr *addr, socklen_t *len); +int getpeername(int fd, struct sockaddr *addr, socklen_t *len); +ssize_t send(int fd, const void *buf, size_t len, int flags); +ssize_t recv(int fd, void *buf, size_t len, int flags); + +#endif diff --git a/foundation/include/pkcl-posix/sys/statfs.h b/foundation/include/pkcl-posix/sys/statfs.h new file mode 100644 index 0000000..1aadb06 --- /dev/null +++ b/foundation/include/pkcl-posix/sys/statfs.h @@ -0,0 +1,24 @@ +#ifndef PKCL_SYS_STATFS_H +#define PKCL_SYS_STATFS_H +#include + +/* FS_VOLUME_INFO (doc/spec_pkcl.md §6.2): block counts of one volume. */ +typedef struct { int __val[2]; } fsid_t; + +struct statfs { + long f_type; + long f_bsize; + unsigned long f_blocks; + unsigned long f_bfree; + unsigned long f_bavail; + unsigned long f_files; + unsigned long f_ffree; + fsid_t f_fsid; + long f_namelen; + long f_frsize; +}; + +int statfs(const char *path, struct statfs *buf); +int fstatfs(int fd, struct statfs *buf); + +#endif diff --git a/foundation/include/pkcl-posix/sys/statvfs.h b/foundation/include/pkcl-posix/sys/statvfs.h new file mode 100644 index 0000000..24e97df --- /dev/null +++ b/foundation/include/pkcl-posix/sys/statvfs.h @@ -0,0 +1,27 @@ +#ifndef PKCL_SYS_STATVFS_H +#define PKCL_SYS_STATVFS_H +#include + +typedef unsigned long fsblkcnt_t; +typedef unsigned long fsfilcnt_t; + +struct statvfs { + unsigned long f_bsize; + unsigned long f_frsize; + fsblkcnt_t f_blocks; + fsblkcnt_t f_bfree; + fsblkcnt_t f_bavail; + fsfilcnt_t f_files; + fsfilcnt_t f_ffree; + fsfilcnt_t f_favail; + unsigned long f_fsid; + unsigned long f_flag; + unsigned long f_namemax; +}; + +#define ST_RDONLY 1 + +int statvfs(const char *path, struct statvfs *buf); +int fstatvfs(int fd, struct statvfs *buf); + +#endif diff --git a/foundation/include/pkcl-posix/sys/sysmacros.h b/foundation/include/pkcl-posix/sys/sysmacros.h new file mode 100644 index 0000000..6b720a3 --- /dev/null +++ b/foundation/include/pkcl-posix/sys/sysmacros.h @@ -0,0 +1,8 @@ +#ifndef PKCL_SYS_SYSMACROS_H +#define PKCL_SYS_SYSMACROS_H + +#define major(dev) (((dev) >> 8) & 0xfff) +#define minor(dev) ((dev) & 0xff) +#define makedev(ma, mi) ((((ma) & 0xfff) << 8) | ((mi) & 0xff)) + +#endif diff --git a/foundation/include/pkcl-posix/sys/termios.h b/foundation/include/pkcl-posix/sys/termios.h new file mode 100644 index 0000000..ccf2635 --- /dev/null +++ b/foundation/include/pkcl-posix/sys/termios.h @@ -0,0 +1,90 @@ +#ifndef PKCL_SYS_TERMIOS_H +#define PKCL_SYS_TERMIOS_H +#include + +/* Terminal attributes over the console's LINE and RAW modes (doc/spec_pkcl.md §6.6). + * Newlib's only includes this file. The values are Linux's. */ +typedef unsigned char cc_t; +typedef unsigned int speed_t; +typedef unsigned int tcflag_t; + +#define NCCS 32 + +struct termios { + tcflag_t c_iflag, c_oflag, c_cflag, c_lflag; + cc_t c_line; + cc_t c_cc[NCCS]; + speed_t c_ispeed, c_ospeed; +}; + +/* c_iflag */ +#define IGNBRK 0x001 +#define BRKINT 0x002 +#define IGNPAR 0x004 +#define PARMRK 0x008 +#define INPCK 0x010 +#define ISTRIP 0x020 +#define INLCR 0x040 +#define IGNCR 0x080 +#define ICRNL 0x100 +#define IXON 0x400 +#define IXOFF 0x1000 +/* c_oflag */ +#define OPOST 0x01 +#define ONLCR 0x04 +/* c_cflag */ +#define CSIZE 0x30 +#define CS8 0x30 +#define CREAD 0x80 +#define HUPCL 0x400 +#define CLOCAL 0x800 +#define PARENB 0x100 +#define PARODD 0x200 +#define CSTOPB 0x40 +/* c_lflag */ +#define ISIG 0x01 +#define ICANON 0x02 +#define ECHO 0x08 +#define ECHOE 0x10 +#define ECHOK 0x20 +#define ECHONL 0x40 +#define NOFLSH 0x80 +#define IEXTEN 0x8000 +#define TOSTOP 0x100 +/* c_cc */ +#define VINTR 0 +#define VQUIT 1 +#define VERASE 2 +#define VKILL 3 +#define VEOF 4 +#define VTIME 5 +#define VMIN 6 +#define VSTART 8 +#define VSTOP 9 +#define VSUSP 10 +/* tcsetattr */ +#define TCSANOW 0 +#define TCSADRAIN 1 +#define TCSAFLUSH 2 +/* tcflush */ +#define TCIFLUSH 0 +#define TCOFLUSH 1 +#define TCIOFLUSH 2 +/* speeds */ +#define B0 0 +#define B9600 13 +#define B38400 15 +#define B115200 0x1002 + +int tcgetattr(int fd, struct termios *t); +int tcsetattr(int fd, int actions, const struct termios *t); +int tcflush(int fd, int queue); +int tcdrain(int fd); +speed_t cfgetispeed(const struct termios *t); +speed_t cfgetospeed(const struct termios *t); +int cfsetispeed(struct termios *t, speed_t speed); +int cfsetospeed(struct termios *t, speed_t speed); +pid_t tcgetpgrp(int fd); +int tcsetpgrp(int fd, pid_t pgrp); + +#endif diff --git a/foundation/include/pkcl-posix/sys/un.h b/foundation/include/pkcl-posix/sys/un.h new file mode 100644 index 0000000..668b3c2 --- /dev/null +++ b/foundation/include/pkcl-posix/sys/un.h @@ -0,0 +1,10 @@ +#ifndef PKCL_SYS_UN_H +#define PKCL_SYS_UN_H +#include + +struct sockaddr_un { + sa_family_t sun_family; + char sun_path[108]; +}; + +#endif diff --git a/foundation/include/pkcl-posix/sys/utsname.h b/foundation/include/pkcl-posix/sys/utsname.h new file mode 100644 index 0000000..9d93b33 --- /dev/null +++ b/foundation/include/pkcl-posix/sys/utsname.h @@ -0,0 +1,15 @@ +#ifndef PKCL_SYS_UTSNAME_H +#define PKCL_SYS_UTSNAME_H + +/* uname(2) (doc/spec_pkcl.md §6.8): the fields are fixed strings of the running Foundation. */ +struct utsname { + char sysname[65]; + char nodename[65]; + char release[65]; + char version[65]; + char machine[65]; +}; + +int uname(struct utsname *name); + +#endif diff --git a/foundation/include/pkcl-posix/sys/vfs.h b/foundation/include/pkcl-posix/sys/vfs.h new file mode 100644 index 0000000..a899db2 --- /dev/null +++ b/foundation/include/pkcl-posix/sys/vfs.h @@ -0,0 +1 @@ +#include diff --git a/foundation/include/pkcl-posix/syslog.h b/foundation/include/pkcl-posix/syslog.h new file mode 100644 index 0000000..ff9821f --- /dev/null +++ b/foundation/include/pkcl-posix/syslog.h @@ -0,0 +1,30 @@ +#ifndef PKCL_SYSLOG_H +#define PKCL_SYSLOG_H +#include + +#define LOG_EMERG 0 +#define LOG_ALERT 1 +#define LOG_CRIT 2 +#define LOG_ERR 3 +#define LOG_WARNING 4 +#define LOG_NOTICE 5 +#define LOG_INFO 6 +#define LOG_DEBUG 7 +#define LOG_USER (1 << 3) +#define LOG_DAEMON (3 << 3) +#define LOG_PID 0x01 +#define LOG_CONS 0x02 +#define LOG_NDELAY 0x08 +#define LOG_PRIMASK 7 +#define LOG_FACMASK 0x3f8 +#define LOG_PRI(p) ((p) & LOG_PRIMASK) +#define LOG_MAKEPRI(f, p) ((f) | (p)) +#define LOG_UPTO(p) ((1 << ((p) + 1)) - 1) + +void openlog(const char *ident, int option, int facility); +void syslog(int priority, const char *format, ...); +void vsyslog(int priority, const char *format, va_list ap); +void closelog(void); +int setlogmask(int mask); + +#endif diff --git a/tools/llvm-src32/driver.py b/tools/llvm-src32/driver.py index f776211..783841f 100644 --- a/tools/llvm-src32/driver.py +++ b/tools/llvm-src32/driver.py @@ -47,7 +47,12 @@ def parser() -> argparse.ArgumentParser: mode = p.add_mutually_exclusive_group() mode.add_argument("-S", action="store_true", help="emit SRC32 assembly") mode.add_argument("--emit-llvm", action="store_true", help="emit linked, verified LLVM IR") + p.add_argument("-c", dest="compile_only", action="store_true", + help="compile the one C/C++ input to LLVM IR (.ll) with this toolchain's flags and stop: no " + "linking, no runtime; for build systems that link the pieces later") p.add_argument("-I", dest="includes", action="append", default=[]) + p.add_argument("--include", dest="forced_includes", action="append", default=[], metavar="FILE", + help="process FILE before the source, like Clang's -include (application code only)") p.add_argument("-D", dest="defines", action="append", default=[]) opt = p.add_mutually_exclusive_group() for level in range(3): @@ -130,7 +135,7 @@ def compile_c(clang: str, source: Path, output: Path, args, defines=(), runtime= command = [clang, "--target=mips-unknown-none", "-mabi=o32", "-msoft-float", "-ffreestanding", "-fno-builtin", "-ffp-contract=off", "-fno-stack-protector", "-fno-pic", "-fno-pie", "-fno-vectorize", "-fno-slp-vectorize", "-fsigned-char", "-G0", "-nostdinc", - "-std=c++17" if cxx else "-std=gnu11" if posix_library else "-std=c11", + "-std=c++17" if cxx else "-std=gnu11" if posix_library or (newlib and newlib.posix) else "-std=c11", optimization, "-S", "-emit-llvm"] if posix_library: if source.is_relative_to(newlib.root / "newlib" / "libm"): @@ -152,6 +157,8 @@ def compile_c(clang: str, source: Path, output: Path, args, defines=(), runtime= if not runtime: for include in args.includes: command.extend(["-I", include]) + for forced in args.forced_includes: + command.extend(["-include", forced]) for define in args.defines: command.extend(["-D", define]) if softfloat: @@ -167,6 +174,19 @@ def compile_c(clang: str, source: Path, output: Path, args, defines=(), runtime= raise DriverError(f"Clang failed for {source}:\n{result.stderr.strip() or result.stdout.strip()}") +def compile_only(args) -> Path: + clang = find_clang(args.clang) + args.newlib = None + if args.newlib_source: + from newlib import Newlib + args.newlib = Newlib(args.newlib_source, clang, args.newlib_heap_size, profile=args.newlib_profile, + cache=args.newlib_cache, jobs=1) + source = args.inputs[0] + dependency_path = args.depfile.resolve() if args.depfile else None + compile_c(clang, source, args.output, args, depfile=dependency_path) + return args.output + + def newlib_posix_defines(): from newlib import POSIX_DEFINES return POSIX_DEFINES @@ -223,6 +243,14 @@ def run(args) -> Path: f'"{ROOT / "requirements.txt"}"') from exc from data import Layout + if args.compile_only: + if len(args.inputs) != 1 or args.inputs[0].suffix.lower() not in SOURCE_SUFFIXES: + raise DriverError("-c takes exactly one C or C++ source") + if args.output is None: + args.output = args.inputs[0].with_suffix(".ll") + elif args.output.suffix.lower() != ".ll": + raise DriverError("-c writes a .ll file") + return compile_only(args) for source in args.inputs: if source.suffix.lower() not in (*SOURCE_SUFFIXES, ".ll", ".bc"): raise DriverError(f"unsupported input extension: {source} (expected .c, .cpp, .cc, .cxx, .ll, or .bc)") diff --git a/tools/pkcl/busybox.config b/tools/pkcl/busybox.config new file mode 100644 index 0000000..8fcf6e9 --- /dev/null +++ b/tools/pkcl/busybox.config @@ -0,0 +1,28 @@ +# Options applied on top of BusyBox's `allnoconfig` (doc/spec_pkcl.md §8). +# One CONFIG_NAME=y|n|"string" per line. BusyBox itself (GPL-2.0) is never +# part of this repository: tools/pkcl/busybox.py takes a source tree you +# fetched yourself. +CONFIG_NOMMU=y +CONFIG_LFS=n +CONFIG_STATIC=y +CONFIG_SHOW_USAGE=y +CONFIG_BUSYBOX=y +CONFIG_FEATURE_INSTALLER=n +CONFIG_CAT=y +CONFIG_ECHO=y +CONFIG_TRUE=y +CONFIG_FALSE=y +CONFIG_PWD=y +CONFIG_BASENAME=y +CONFIG_DIRNAME=y +CONFIG_HEAD=y +CONFIG_WC=y +CONFIG_MKDIR=y +CONFIG_RMDIR=y +CONFIG_RM=y +CONFIG_CP=y +CONFIG_MV=y +CONFIG_LS=y +CONFIG_SLEEP=y +CONFIG_TEST=y +CONFIG_PRINTF=y diff --git a/tools/pkcl/busybox.py b/tools/pkcl/busybox.py new file mode 100644 index 0000000..d177a8c --- /dev/null +++ b/tools/pkcl/busybox.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +"""Build BusyBox for Foundation with pkcl and Newlib (doc/spec_pkcl.md §8). + +BusyBox is GPL-2.0 and not part of this repository: pass the root of a source +tree you obtained yourself (and the same for Newlib, see +examples/newlib-c/README.md). + + python tools/pkcl/busybox.py --busybox-source DIR --newlib-source DIR \\ + --fnd-wal root/lib/fnd.1.wal --out build/busybox + +Steps: (1) configure BusyBox out of tree with tools/pkcl/busybox.config and run +BusyBox's own build once with the host compiler, which generates its headers +and tells us exactly which sources it compiles and with which flags; (2) compile +each of those sources to LLVM IR with tools/llvm-src32 (-c); (3) link the IR, +pkcl and Newlib into busybox.wax. Needs the host's gcc, make and bison. +""" +from __future__ import annotations + +import argparse +from concurrent.futures import ThreadPoolExecutor +import os +from pathlib import Path +import re +import shlex +import subprocess +import sys + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] +DRIVER = ROOT / "tools/llvm-src32/driver.py" +PKCL = ROOT / "foundation/lib/pkcl" +# Slot 5 of the user slots (spec_Foundation.md §5.2) with a 64 KiB user stack. +LAYOUT = ["--origin", "0x800000", "--stack-top", "0xC00000", "--user-stack-size", "0x20000"] + + +def run(command, cwd=None, log=None, check=True): + result = subprocess.run(command, cwd=cwd, capture_output=True, text=True, errors="replace") + if log: + Path(log).write_text(result.stdout + result.stderr, encoding="utf-8") + if check and result.returncode: + raise SystemExit(f"{' '.join(map(str, command[:3]))}…: exit {result.returncode}\n" + f"{(result.stderr or result.stdout)[-3000:]}") + return result + + +def configure(args, build: Path): + build.mkdir(parents=True, exist_ok=True) + make = ["make", "-C", str(args.busybox_source), f"O={build}"] + run([*make, "allnoconfig"], log=build / "allnoconfig.log") + wanted = {} + for line in args.config.read_text(encoding="utf-8").splitlines(): + line = line.split("#", 1)[0].strip() if not line.startswith("CONFIG_") else line.strip() + if "=" in line: + key, value = line.split("=", 1) + wanted[key] = value + lines, seen = [], set() + for line in (build / ".config").read_text(encoding="utf-8").splitlines(): + match = re.match(r"# (CONFIG_\w+) is not set", line) or re.match(r"(CONFIG_\w+)=", line) + if match and match[1] in wanted: + key = match[1] + lines.append(f"# {key} is not set" if wanted[key] == "n" else f"{key}={wanted[key]}") + seen.add(key) + else: + lines.append(line) + lines += [f"{k}={v}" for k, v in wanted.items() if k not in seen and v != "n"] + (build / ".config").write_text("\n".join(lines) + "\n", encoding="utf-8") + run(["sh", "-c", f"yes n | {shlex.join(make)} oldconfig"], log=build / "oldconfig.log") + # Hard dependencies can switch an option back off; say so rather than build something else. + final = (build / ".config").read_text(encoding="utf-8") + for key, value in wanted.items(): + if value != "n" and f"{key}={value}" not in final: + print(f"warning: {key}={value} was not accepted by BusyBox's dependency rules", file=sys.stderr) + run([*make, "V=1", f"-j{args.jobs}"], log=build / "host-build.log", check=False) + if not (build / "include/autoconf.h").is_file(): + raise SystemExit(f"BusyBox's host build failed; see {build / 'host-build.log'}") + + +def target_sources(build: Path): + """(source, flags) of every translation unit linked into busybox, from the host build's log.""" + commands = {} + for line in (build / "host-build.log").read_text(encoding="utf-8", errors="replace").splitlines(): + line = line.strip() + if not re.match(r"(gcc|cc) .* -c ", line) or "echo " in line[:8]: + continue + words = shlex.split(line) + if "-o" not in words: + continue + obj = words[words.index("-o") + 1] + source = words[-1] + # Assembly and the host-architecture variants (SHA hardware acceleration) are not for this target. + if not source.endswith(".c") or re.search(r"_(x86|arm|aarch)", Path(source).name): + continue + flags = [] + index = 1 + while index < len(words) - 1: + word = words[index] + if word in ("-include", "-I", "-D", "-o"): + flags.append((word, words[index + 1])) + index += 2 + continue + for prefix in ("-I", "-D"): + if word.startswith(prefix) and len(word) > 2: + flags.append((prefix, word[2:])) + index += 1 + commands[obj] = (source, flags) + # Objects that end up in the final link: members of each lib.a, plus applets.o. + members = {"applets/applets.o"} + for archive in sorted(build.rglob("lib.a")): + listing = run(["ar", "t", str(archive)], check=False).stdout.split() + members |= {str(archive.parent.relative_to(build) / member) for member in listing} + chosen = {obj: commands[obj] for obj in members if obj in commands} + missing = sorted(members - set(chosen)) + if missing: + print("warning: no compile command for", ", ".join(missing), file=sys.stderr) + return chosen + + +def driver_args(args): + return ["--newlib-source", str(args.newlib_source), "--newlib-profile", "posix", + "--newlib-cache", str(args.cache), *(["--clang", args.clang] if args.clang else [])] + + +def compile_all(args, build: Path, out: Path): + out.mkdir(parents=True, exist_ok=True) + units = target_sources(build) + if args.only: + units = {obj: unit for obj, unit in units.items() if re.search(args.only, obj)} + failures = {} + + def compile_one(item): + obj, (source, flags) = item + ir = out / (obj.replace("/", "-")[:-2] + ".ll") + command = [sys.executable, str(DRIVER), "-c", source, args.optimization, + "-D", "__clang_major__=0", # インラインアセンブリを使う Clang 向け分岐を避ける + "-I", str(ROOT / "foundation/include/pkcl-posix"), *driver_args(args), "-o", str(ir)] + for kind, value in flags: + if kind == "-I": + command += ["-I", str((build / value).resolve())] + elif kind == "-D": + command += ["-D", value] + elif kind == "-include": + command += ["--include", str((build / value).resolve())] + result = run(command, cwd=build, check=False) + if result.returncode: + failures[obj] = (result.stderr or result.stdout).strip() + return None + return ir + + with ThreadPoolExecutor(max_workers=args.jobs) as pool: + irs = [ir for ir in pool.map(compile_one, sorted(units.items())) if ir] + summary = {} + for obj, text in sorted(failures.items()): + first = [m[1] for m in (re.search(r":\d+:\d+: (?:fatal )?error: (.*)", line) for line in text.splitlines()) if m] + key = first[0] if first else text.splitlines()[0][:120] + summary.setdefault(key, []).append(obj) + for key, objs in sorted(summary.items(), key=lambda item: -len(item[1])): + print(f"{len(objs):4d} {key} [{', '.join(objs[:3])}{'…' if len(objs) > 3 else ''}]", file=sys.stderr) + print(f"compiled {len(irs)} of {len(units)} translation units") + return irs, failures + + +def link(args, irs, out: Path): + sources = [PKCL / name for name in ("errno.c", "fd.c", "fs.c", "crt.c", "newlib/syscalls.c")] + extra = sorted(PKCL.glob("posix/*.c")) + command = [sys.executable, str(DRIVER), *map(str, irs), *map(str, sources), *map(str, extra), args.optimization, + "-I", str(ROOT / "foundation/include"), "-I", str(ROOT / "sdk/cpt32/include"), + "-I", str(ROOT / "foundation/include/pkcl-posix"), "-D", "PKCL_LIBC", + *driver_args(args), "-j", str(args.jobs), "--entry", "pkcl_start", "--wax3", "--page-align", + "--image-id", "busybox", "--import-wal", str(args.fnd_wal), *LAYOUT, "-o", str(out / "busybox.wax")] + result = run(command, check=False) + print((result.stdout + result.stderr).strip()[-1500:]) + return result.returncode + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--busybox-source", type=Path, required=True) + parser.add_argument("--newlib-source", type=Path, required=True) + parser.add_argument("--fnd-wal", type=Path, required=True, help="/lib/fnd.1.wal built by foundation/boot/build.py") + parser.add_argument("--out", type=Path, default=Path("build/busybox")) + parser.add_argument("--config", type=Path, default=HERE / "busybox.config") + parser.add_argument("--clang") + parser.add_argument("--cache", type=Path, help="Newlib IR cache (default: tools/llvm-src32's)") + parser.add_argument("-O", dest="optimization", default="-O1", choices=["-O0", "-O1", "-O2"]) + parser.add_argument("-j", "--jobs", type=int, default=os.cpu_count() or 1) + parser.add_argument("--only", help="regular expression: compile only the matching objects (debugging)") + parser.add_argument("--stage", choices=["configure", "compile", "link"], default="link") + args = parser.parse_args(argv) + args.busybox_source = args.busybox_source.resolve() + args.out = args.out.resolve() + build = args.out / "bb" + if not (build / "include/autoconf.h").is_file(): + configure(args, build) + if args.stage == "configure": + return 0 + irs, failures = compile_all(args, build, args.out / "ir") + if failures or args.stage == "compile": + return 1 if failures else 0 + return link(args, irs, args.out) + + +if __name__ == "__main__": + raise SystemExit(main()) From 99d512005fec01174eb3a1e59bacb94b036f2a19 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 10:04:21 +0000 Subject: [PATCH 4/5] =?UTF-8?q?pkcl:=20BusyBox=20=E3=81=8C=20Foundation=20?= =?UTF-8?q?=E4=B8=8A=E3=81=A7=E3=83=AA=E3=83=B3=E3=82=AF=E3=83=BB=E8=B5=B7?= =?UTF-8?q?=E5=8B=95=20(--help)=E3=80=81=E3=83=90=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=83=89=E3=81=A8=20Newlib=20=E3=81=AE?= =?UTF-8?q?=E7=B8=AE=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FqgVXKR79fR8rybAwBZ2fY --- foundation/include/pkcl-posix/glob.h | 7 + foundation/include/pkcl-posix/netdb.h | 9 + foundation/include/pkcl-posix/signal.h | 14 ++ foundation/include/pkcl-posix/stdio.h | 6 + foundation/include/pkcl-posix/stdlib.h | 1 + foundation/include/pkcl-posix/sys/resource.h | 18 ++ foundation/include/pkcl-posix/sys/socket.h | 1 + foundation/include/pkcl-posix/sys/stat.h | 7 + foundation/include/pkcl-posix/sys/termios.h | 17 ++ foundation/include/pkcl-posix/time.h | 5 + foundation/lib/pkcl/posix/dirent.c | 71 +++++++ foundation/lib/pkcl/posix/misc.c | 201 +++++++++++++++++++ foundation/lib/pkcl/posix/net.c | 74 +++++++ foundation/lib/pkcl/posix/proc.c | 198 ++++++++++++++++++ foundation/lib/pkcl/posix/setjmp.c | 14 ++ foundation/lib/pkcl/posix/term.c | 121 +++++++++++ tools/llvm-src32/backend.py | 145 ++++++++++++- tools/llvm-src32/data.py | 2 +- tools/llvm-src32/driver.py | 8 +- tools/llvm-src32/newlib.py | 31 ++- tools/pkcl/busybox.config | 4 + tools/pkcl/busybox.py | 17 +- 22 files changed, 962 insertions(+), 9 deletions(-) create mode 100644 foundation/include/pkcl-posix/glob.h create mode 100644 foundation/include/pkcl-posix/signal.h create mode 100644 foundation/include/pkcl-posix/stdio.h create mode 100644 foundation/include/pkcl-posix/sys/resource.h create mode 100644 foundation/include/pkcl-posix/sys/stat.h create mode 100644 foundation/include/pkcl-posix/time.h create mode 100644 foundation/lib/pkcl/posix/dirent.c create mode 100644 foundation/lib/pkcl/posix/misc.c create mode 100644 foundation/lib/pkcl/posix/net.c create mode 100644 foundation/lib/pkcl/posix/proc.c create mode 100644 foundation/lib/pkcl/posix/setjmp.c create mode 100644 foundation/lib/pkcl/posix/term.c diff --git a/foundation/include/pkcl-posix/glob.h b/foundation/include/pkcl-posix/glob.h new file mode 100644 index 0000000..5acb60f --- /dev/null +++ b/foundation/include/pkcl-posix/glob.h @@ -0,0 +1,7 @@ +#ifndef PKCL_GLOB_H +#define PKCL_GLOB_H +#include_next +#ifndef GLOB_NOMATCH +#define GLOB_NOMATCH (-3) +#endif +#endif diff --git a/foundation/include/pkcl-posix/netdb.h b/foundation/include/pkcl-posix/netdb.h index aab6c93..867db35 100644 --- a/foundation/include/pkcl-posix/netdb.h +++ b/foundation/include/pkcl-posix/netdb.h @@ -27,10 +27,19 @@ struct addrinfo { #define AI_NUMERICHOST 4 #define NI_NUMERICHOST 1 #define NI_NUMERICSERV 2 +#define NI_NAMEREQD 8 +extern int h_errno; +#define HOST_NOT_FOUND 1 +#define TRY_AGAIN 2 +#define NO_RECOVERY 3 +#define NO_DATA 4 #define EAI_NONAME (-2) #define EAI_FAIL (-4) #define EAI_SYSTEM (-11) +struct servent { char *s_name; char **s_aliases; int s_port; char *s_proto; }; +struct servent *getservbyname(const char *name, const char *proto); +const char *hstrerror(int err); struct hostent *gethostbyname(const char *name); int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); void freeaddrinfo(struct addrinfo *res); diff --git a/foundation/include/pkcl-posix/signal.h b/foundation/include/pkcl-posix/signal.h new file mode 100644 index 0000000..ac33c16 --- /dev/null +++ b/foundation/include/pkcl-posix/signal.h @@ -0,0 +1,14 @@ +#ifndef PKCL_SIGNAL_H +#define PKCL_SIGNAL_H +#include_next +#ifndef SA_RESTART +#define SA_RESTART 0x10000000 +#endif +#ifndef SA_NODEFER +#define SA_NODEFER 0x40000000 +#endif +#ifndef SA_RESETHAND +#define SA_RESETHAND 0x80000000 +#endif +int sigisemptyset(const sigset_t *set); +#endif diff --git a/foundation/include/pkcl-posix/stdio.h b/foundation/include/pkcl-posix/stdio.h new file mode 100644 index 0000000..2f0e67e --- /dev/null +++ b/foundation/include/pkcl-posix/stdio.h @@ -0,0 +1,6 @@ +#ifndef PKCL_STDIO_H +#define PKCL_STDIO_H +#include_next +#include +ssize_t getline(char **line, size_t *size, FILE *stream); +#endif diff --git a/foundation/include/pkcl-posix/stdlib.h b/foundation/include/pkcl-posix/stdlib.h index b46f0c9..602727e 100644 --- a/foundation/include/pkcl-posix/stdlib.h +++ b/foundation/include/pkcl-posix/stdlib.h @@ -5,3 +5,4 @@ #include_next #undef itoa #undef utoa +int clearenv(void); diff --git a/foundation/include/pkcl-posix/sys/resource.h b/foundation/include/pkcl-posix/sys/resource.h new file mode 100644 index 0000000..7d12049 --- /dev/null +++ b/foundation/include/pkcl-posix/sys/resource.h @@ -0,0 +1,18 @@ +#ifndef PKCL_SYS_RESOURCE_H +#define PKCL_SYS_RESOURCE_H +#include_next +#ifndef RLIMIT_CORE +typedef unsigned long rlim_t; +struct rlimit { rlim_t rlim_cur; rlim_t rlim_max; }; +#define RLIM_INFINITY ((rlim_t)-1) +#define RLIMIT_CPU 0 +#define RLIMIT_FSIZE 1 +#define RLIMIT_DATA 2 +#define RLIMIT_STACK 3 +#define RLIMIT_CORE 4 +#define RLIMIT_NOFILE 7 +#define RLIMIT_AS 9 +int getrlimit(int resource, struct rlimit *rlim); +int setrlimit(int resource, const struct rlimit *rlim); +#endif +#endif diff --git a/foundation/include/pkcl-posix/sys/socket.h b/foundation/include/pkcl-posix/sys/socket.h index d628cd9..8203a2c 100644 --- a/foundation/include/pkcl-posix/sys/socket.h +++ b/foundation/include/pkcl-posix/sys/socket.h @@ -57,6 +57,7 @@ int getsockopt(int fd, int level, int name, void *value, socklen_t *len); int getsockname(int fd, struct sockaddr *addr, socklen_t *len); int getpeername(int fd, struct sockaddr *addr, socklen_t *len); ssize_t send(int fd, const void *buf, size_t len, int flags); +ssize_t sendto(int fd, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen); ssize_t recv(int fd, void *buf, size_t len, int flags); #endif diff --git a/foundation/include/pkcl-posix/sys/stat.h b/foundation/include/pkcl-posix/sys/stat.h new file mode 100644 index 0000000..c02d075 --- /dev/null +++ b/foundation/include/pkcl-posix/sys/stat.h @@ -0,0 +1,7 @@ +#ifndef PKCL_SYS_STAT_H +#define PKCL_SYS_STAT_H +#include_next +/* pkcl は symlink を持たないので lstat は stat と同じ (doc/spec_pkcl.md §6) */ +int mknod(const char *path, mode_t mode, dev_t dev); +int lstat(const char *path, struct stat *st); +#endif diff --git a/foundation/include/pkcl-posix/sys/termios.h b/foundation/include/pkcl-posix/sys/termios.h index ccf2635..9dbe3b1 100644 --- a/foundation/include/pkcl-posix/sys/termios.h +++ b/foundation/include/pkcl-posix/sys/termios.h @@ -75,6 +75,23 @@ struct termios { #define B9600 13 #define B38400 15 #define B115200 0x1002 +#define B50 1 +#define B75 2 +#define B110 3 +#define B134 4 +#define B150 5 +#define B200 6 +#define B300 7 +#define B600 8 +#define B1200 9 +#define B1800 10 +#define B2400 11 +#define B4800 12 +#define B19200 14 +#define B57600 4097 +#define B230400 4099 +#define B460800 4100 +#define B921600 4103 int tcgetattr(int fd, struct termios *t); int tcsetattr(int fd, int actions, const struct termios *t); diff --git a/foundation/include/pkcl-posix/time.h b/foundation/include/pkcl-posix/time.h new file mode 100644 index 0000000..e07bd64 --- /dev/null +++ b/foundation/include/pkcl-posix/time.h @@ -0,0 +1,5 @@ +#ifndef PKCL_TIME_H +#define PKCL_TIME_H +#include_next +int nanosleep(const struct timespec *req, struct timespec *rem); +#endif diff --git a/foundation/lib/pkcl/posix/dirent.c b/foundation/lib/pkcl/posix/dirent.c new file mode 100644 index 0000000..dcb2b88 --- /dev/null +++ b/foundation/lib/pkcl/posix/dirent.c @@ -0,0 +1,71 @@ +/* ディレクトリストリーム(doc/spec_pkcl.md §6.2)。fnd_dir_open / fnd_dir_next の上に作る。 */ +#define _GNU_SOURCE 1 +#include +#include +#include +#include + +#include +#include + +struct pkcl_dir { + fnd_handle handle; + struct dirent entry; + fnd_dirent_t raw; +}; + +DIR *opendir(const char *path) { + pkcl_init(); + DIR *dir = malloc(sizeof *dir); + if (!dir) { + errno = ENOMEM; + return 0; + } + if (pkcl_fail(fnd_dir_open(path, &dir->handle))) { + free(dir); + return 0; + } + return dir; +} + +int readdir_r(DIR *dir, struct dirent *entry, struct dirent **result) { + fnd_status status = fnd_dir_next(dir->handle, &dir->raw); + if (status == FND_END) { + *result = 0; + return 0; + } + if (status < 0) { + *result = 0; + return pkcl_errno(status); + } + entry->d_ino = 0; + entry->d_type = dir->raw.stat.type == FND_TYPE_DIR ? DT_DIR + : dir->raw.stat.type == FND_TYPE_FILE ? DT_REG + : dir->raw.stat.type == FND_TYPE_CONSOLE ? DT_CHR : DT_UNKNOWN; + strncpy(entry->d_name, dir->raw.name, NAME_MAX); + entry->d_name[NAME_MAX] = 0; + *result = entry; + return 0; +} + +struct dirent *readdir(DIR *dir) { + struct dirent *result; + int error = readdir_r(dir, &dir->entry, &result); + if (error) + errno = error; + return result; +} + +void rewinddir(DIR *dir) { fnd_dir_rewind(dir->handle); } + +int closedir(DIR *dir) { + fnd_status status = fnd_close(dir->handle); + free(dir); + return pkcl_fail(status); +} + +int dirfd(DIR *dir) { + (void)dir; + errno = ENOTSUP; /* ディレクトリは fd 表に載らない */ + return -1; +} diff --git a/foundation/lib/pkcl/posix/misc.c b/foundation/lib/pkcl/posix/misc.c new file mode 100644 index 0000000..2bc3255 --- /dev/null +++ b/foundation/lib/pkcl/posix/misc.c @@ -0,0 +1,201 @@ +/* ファイル系の残り・ユーザー DB・パターン照合(doc/spec_pkcl.md §6)。 */ +#define _GNU_SOURCE 1 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/* ---- リンク類: FS2 にはない ---- */ +int link(const char *a, const char *b) { (void)a, (void)b; errno = EPERM; return -1; } +int symlink(const char *a, const char *b) { (void)a, (void)b; errno = EPERM; return -1; } +ssize_t readlink(const char *p, char *b, size_t n) { (void)p, (void)b, (void)n; errno = EINVAL; return -1; } +int mknod(const char *p, mode_t m, dev_t d) { (void)p, (void)m, (void)d; errno = EPERM; return -1; } +int chroot(const char *p) { (void)p; errno = EPERM; return -1; } +int fchdir(int fd) { (void)fd; errno = ENOSYS; return -1; } +/* 所有者は 1 人だけ(root)なので、存在するパスなら成功にする。 */ +int chown(const char *p, uid_t u, gid_t g) { (void)u, (void)g; struct stat st; return stat(p, &st); } +int lchown(const char *p, uid_t u, gid_t g) { return chown(p, u, g); } +/* §7 G5: fs に時刻の更新がないので、存在確認だけして成功にする(時刻は変わらない)。 */ +int utimes(const char *p, const struct timeval tv[2]) { (void)tv; struct stat st; return stat(p, &st); } + +void *mmap(void *a, size_t n, int p, int f, int fd, off_t o) { + (void)a, (void)n, (void)p, (void)f, (void)fd, (void)o; + errno = ENOSYS; + return (void *)-1; +} +int munmap(void *a, size_t n) { (void)a, (void)n; errno = EINVAL; return -1; } +int mallopt(int p, int v) { (void)p, (void)v; return 1; } +int clearenv(void) { extern char **environ; if (environ) environ[0] = 0; return 0; } + +/* ---- パス ---- */ +char *dirname(char *path) { + if (!path || !*path) + return (char *)"."; + size_t n = strlen(path); + while (n > 1 && path[n - 1] == '/') + path[--n] = 0; + char *slash = strrchr(path, '/'); + if (!slash) + return (char *)"."; + while (slash > path && slash[-1] == '/') + --slash; + if (slash == path) + path[1] = 0; + else + *slash = 0; + return path; +} + +/* symlink がないので、"." と ".." と重複した "/" を畳んで存在を確かめるだけでよい。 */ +char *realpath(const char *path, char *out) { + char full[PKCL_PATH_MAX + 2]; + size_t n = 0; + if (path[0] != '/') { + if (!getcwd(full, sizeof full)) + return 0; + n = strlen(full); + } + if (n + 1 + strlen(path) >= sizeof full) { + errno = ENAMETOOLONG; + return 0; + } + full[n++] = '/'; + strcpy(full + n, path); + char *result = out ? out : malloc(PKCL_PATH_MAX + 1); + if (!result) { + errno = ENOMEM; + return 0; + } + size_t len = 0; + for (char *p = full; *p;) { + char *end = strchr(p, '/'); + size_t part = end ? (size_t)(end - p) : strlen(p); + if (part == 0 || (part == 1 && p[0] == '.')) { + } else if (part == 2 && p[0] == '.' && p[1] == '.') { + while (len > 0 && result[--len] != '/') {} + } else { + result[len++] = '/'; + memcpy(result + len, p, part); + len += part; + } + p += part + (end != 0); + } + if (len == 0) + result[len++] = '/'; + result[len] = 0; + struct stat st; + if (stat(result, &st) < 0) { + if (!out) + free(result); + return 0; + } + return result; +} + +/* ---- パターン照合 ---- */ +static int match_here(const char *pat, const char *s, int flags, const char *start) { + for (; *pat; ++pat) { + if (*pat == '*') { + while (pat[1] == '*') + ++pat; + if ((flags & FNM_PERIOD) && *s == '.' && s == start) + return FNM_NOMATCH; + for (;; ++s) { + if (match_here(pat + 1, s, flags, start) == 0) + return 0; + if (!*s || ((flags & FNM_PATHNAME) && *s == '/')) + return FNM_NOMATCH; + } + } + if (!*s) + return FNM_NOMATCH; + if ((flags & FNM_PATHNAME) && *s == '/' && *pat != '/') + return FNM_NOMATCH; + if ((flags & FNM_PERIOD) && *s == '.' && s == start && *pat != '.') + return FNM_NOMATCH; + if (*pat == '?') { + ++s; + } else if (*pat == '[') { + const char *p = pat + 1; + int negate = *p == '!' || *p == '^', hit = 0; + p += negate; + for (int first = 1; *p && (*p != ']' || first); first = 0) { + char lo = *p++, hi = lo; + if (lo == '\\' && !(flags & FNM_NOESCAPE) && *p) + lo = hi = *p++; + if (*p == '-' && p[1] && p[1] != ']') { + hi = p[1]; + p += 2; + } + hit |= (unsigned char)*s >= (unsigned char)lo && (unsigned char)*s <= (unsigned char)hi; + } + if (*p != ']' || hit == negate) + return FNM_NOMATCH; + pat = p; + ++s; + } else { + if (*pat == '\\' && !(flags & FNM_NOESCAPE) && pat[1]) + ++pat; + if (*pat != *s) + return FNM_NOMATCH; + ++s; + } + if (pat[0] == 0) + break; + } + return *s ? FNM_NOMATCH : 0; +} + +int fnmatch(const char *pattern, const char *string, int flags) { + return match_here(pattern, string, flags, string); +} + +/* §6 の残り: ディレクトリを辿る glob はまだない。パターンに特殊文字がなければその名前だけ返す。 */ +int glob(const char *pattern, int flags, int (*errfunc)(const char *, int), glob_t *g) { + (void)errfunc; + struct stat st; + if (strpbrk(pattern, "*?[") || stat(pattern, &st) < 0) + return GLOB_NOMATCH; + size_t offs = (flags & GLOB_DOOFFS) ? g->gl_offs : 0; + g->gl_pathv = calloc(offs + 2, sizeof(char *)); + if (!g->gl_pathv) + return GLOB_NOSPACE; + g->gl_pathv[offs] = strdup(pattern); + g->gl_pathc = 1; + return 0; +} +void globfree(glob_t *g) { + for (size_t i = 0; g->gl_pathv && i < g->gl_pathc; ++i) + free(g->gl_pathv[i]); + free(g->gl_pathv); + g->gl_pathv = 0; +} + +/* ---- ユーザー DB: root だけ ---- */ +static char root_name[] = "root", root_dir[] = "/", root_shell[] = "/bin/sh", empty[] = ""; +static struct passwd root_pw = {root_name, empty, 0, 0, empty, root_name, root_dir, root_shell}; +static char *no_members[1]; +static struct group root_gr = {root_name, empty, 0, no_members}; + +struct passwd *getpwuid(uid_t uid) { return uid == 0 ? &root_pw : 0; } +struct passwd *getpwnam(const char *name) { return strcmp(name, "root") == 0 ? &root_pw : 0; } +struct group *getgrgid(gid_t gid) { return gid == 0 ? &root_gr : 0; } +struct group *getgrnam(const char *name) { return strcmp(name, "root") == 0 ? &root_gr : 0; } +void endgrent(void) {} + +/* Newlib はこの 2 つを __ 付きの名前で持つ(ヘッダが POSIX の名前に写す)。 */ +#include +extern ssize_t __getline(char **, size_t *, FILE *); +extern ssize_t __getdelim(char **, size_t *, int, FILE *); +ssize_t getline(char **line, size_t *size, FILE *stream) { return __getline(line, size, stream); } +ssize_t getdelim(char **line, size_t *size, int delim, FILE *stream) { return __getdelim(line, size, delim, stream); } diff --git a/foundation/lib/pkcl/posix/net.c b/foundation/lib/pkcl/posix/net.c new file mode 100644 index 0000000..351a4c9 --- /dev/null +++ b/foundation/lib/pkcl/posix/net.c @@ -0,0 +1,74 @@ +/* ネットワークはない(doc/spec_pkcl.md §6.8)。BusyBox がリンクできるように、失敗する定義だけを置く。 */ +#define _GNU_SOURCE 1 +#include +#include +#include +#include + +int h_errno; + +static int unsupported(void) { + errno = ENOSYS; + return -1; +} +int socket(int d, int t, int p) { (void)d, (void)t, (void)p; return unsupported(); } +int bind(int fd, const struct sockaddr *a, socklen_t n) { (void)fd, (void)a, (void)n; return unsupported(); } +int connect(int fd, const struct sockaddr *a, socklen_t n) { (void)fd, (void)a, (void)n; return unsupported(); } +int listen(int fd, int n) { (void)fd, (void)n; return unsupported(); } +int getsockname(int fd, struct sockaddr *a, socklen_t *n) { (void)fd, (void)a, (void)n; return unsupported(); } +int getpeername(int fd, struct sockaddr *a, socklen_t *n) { (void)fd, (void)a, (void)n; return unsupported(); } +int setsockopt(int fd, int l, int o, const void *v, socklen_t n) { (void)fd, (void)l, (void)o, (void)v, (void)n; return unsupported(); } +ssize_t sendto(int fd, const void *b, size_t n, int f, const struct sockaddr *a, socklen_t l) { + (void)fd, (void)b, (void)n, (void)f, (void)a, (void)l; + return unsupported(); +} + +struct hostent *gethostbyname(const char *name) { (void)name; h_errno = HOST_NOT_FOUND; return 0; } +struct servent *getservbyname(const char *name, const char *proto) { (void)name, (void)proto; return 0; } +int getaddrinfo(const char *n, const char *s, const struct addrinfo *h, struct addrinfo **r) { + (void)n, (void)s, (void)h, (void)r; + return EAI_FAIL; +} +void freeaddrinfo(struct addrinfo *r) { (void)r; } +int getnameinfo(const struct sockaddr *a, socklen_t al, char *h, socklen_t hl, char *s, socklen_t sl, int f) { + (void)a, (void)al, (void)h, (void)hl, (void)s, (void)sl, (void)f; + return EAI_FAIL; +} +const char *gai_strerror(int code) { (void)code; return "name resolution is not available"; } +const char *hstrerror(int code) { (void)code; return "host not found"; } + +int inet_aton(const char *text, struct in_addr *out) { + uint32_t value = 0; + for (int part = 0; part < 4; ++part) { + if (*text < '0' || *text > '9') + return 0; + unsigned number = 0; + while (*text >= '0' && *text <= '9') + number = number * 10 + (unsigned)(*text++ - '0'); + if (number > 255 || (part < 3 && *text++ != '.')) + return 0; + value = value << 8 | number; + } + if (*text) + return 0; + out->s_addr = htonl(value); + return 1; +} + +char *inet_ntoa(struct in_addr in) { + static char text[16]; + uint32_t value = ntohl(in.s_addr); + char *p = text; + for (int shift = 24; shift >= 0; shift -= 8) { + unsigned byte = (value >> shift) & 255u; + if (byte >= 100) + *p++ = (char)('0' + byte / 100); + if (byte >= 10) + *p++ = (char)('0' + byte / 10 % 10); + *p++ = (char)('0' + byte % 10); + if (shift) + *p++ = '.'; + } + *p = 0; + return text; +} diff --git a/foundation/lib/pkcl/posix/proc.c b/foundation/lib/pkcl/posix/proc.c new file mode 100644 index 0000000..e8fa216 --- /dev/null +++ b/foundation/lib/pkcl/posix/proc.c @@ -0,0 +1,198 @@ +/* プロセス・時間・シグナル・資源制限(doc/spec_pkcl.md §6.3〜§6.5, §7)。 + * Foundation には fork も signal もないので、必要な範囲で縮退させる。 */ +#define _GNU_SOURCE 1 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +extern char **environ; + +/* ---- 実行 ---- + * exec 族は「子を起動して終了を待ち、その終了コードで自分も終わる」で置き換える。 + * 呼び出し元から見ると戻らない点が exec と同じで、違うのは pid とメモリが残ることだけ。 */ +int execve(const char *path, char *const argv[], char *const envp[]) { + (void)envp; /* §7 G2: 環境ブロックはまだ渡せない */ + pkcl_init(); + uint32_t argc = 0; + while (argv && argv[argc]) + ++argc; + fnd_exec_t ex = {0}; + ex.argc = argc; + ex.argv = (const char *const *)argv; + ex.std_in = pkcl_handle(0); + ex.std_out = pkcl_handle(1); + ex.std_err = pkcl_handle(2); + fnd_handle proc; + if (pkcl_fail(fnd_proc_exec(path, &ex, &proc))) + return -1; + int32_t code = 0; + fnd_proc_wait(proc, &code); + fnd_close(proc); + exit(code); +} + +int execv(const char *path, char *const argv[]) { return execve(path, argv, environ); } + +int execvp(const char *file, char *const argv[]) { + if (strchr(file, '/')) + return execv(file, argv); + const char *path = getenv("PATH"); + if (!path) + path = "/bin"; + char buf[PKCL_PATH_MAX + 1]; + while (*path) { + const char *end = strchr(path, ':'); + size_t length = end ? (size_t)(end - path) : strlen(path); + if (length + 1 + strlen(file) < sizeof buf) { + memcpy(buf, path, length); + buf[length] = '/'; + strcpy(buf + length + 1, file); + fnd_stat_t st; + if (fnd_path_stat(buf, &st) == FND_OK && st.type == FND_TYPE_FILE) + return execv(buf, argv); + } + path = end ? end + 1 : path + length; + } + errno = ENOENT; + return -1; +} + +pid_t vfork(void) { + errno = ENOSYS; + return -1; +} +pid_t wait(int *status) { + (void)status; + errno = ECHILD; + return -1; +} +pid_t waitpid(pid_t pid, int *status, int options) { + (void)pid, (void)status, (void)options; + errno = ECHILD; + return -1; +} + +pid_t getpid(void) { return 1; } +pid_t getppid(void) { return 0; } +pid_t setsid(void) { return 1; } +int setuid(uid_t uid) { return uid == 0 ? 0 : (errno = EPERM, -1); } +int setgid(gid_t gid) { return gid == 0 ? 0 : (errno = EPERM, -1); } +int seteuid(uid_t uid) { return setuid(uid); } +int setegid(gid_t gid) { return setgid(gid); } +int getgroups(int size, gid_t *list) { + (void)size, (void)list; + return 0; +} +int initgroups(const char *user, gid_t group) { + (void)user, (void)group; + return 0; +} + +/* ---- 時間 ---- */ +int gettimeofday(struct timeval *tv, void *tz) { + extern int _gettimeofday(struct timeval *, void *); + return _gettimeofday(tv, tz); +} +int settimeofday(const struct timeval *tv, const struct timezone *tz) { + (void)tv, (void)tz; + errno = EPERM; + return -1; +} + +static void sleep_us(unsigned long long us) { + uint32_t hz = (uint32_t)fnd_sys_time_get(2); + if (!hz) + hz = 100; + unsigned long long ticks = (us * hz + 999999u) / 1000000u; + if (ticks > 0x7FFFFFFFu) + ticks = 0x7FFFFFFFu; + fnd_sys_sleep((uint32_t)ticks); +} +unsigned sleep(unsigned seconds) { + sleep_us((unsigned long long)seconds * 1000000u); + return 0; +} +int usleep(useconds_t us) { + sleep_us(us); + return 0; +} +int nanosleep(const struct timespec *req, struct timespec *rem) { + if (req->tv_sec < 0 || req->tv_nsec < 0 || req->tv_nsec >= 1000000000L) { + errno = EINVAL; + return -1; + } + sleep_us((unsigned long long)req->tv_sec * 1000000u + (unsigned long long)req->tv_nsec / 1000u); + if (rem) + rem->tv_sec = rem->tv_nsec = 0; + return 0; +} +unsigned alarm(unsigned seconds) { + (void)seconds; /* SIGALRM を届ける手段がない(§7 G-signal) */ + return 0; +} + +/* ---- シグナル: ハンドラは登録できるが、配送はされない ---- */ +int sigaction(int sig, const struct sigaction *act, struct sigaction *old) { + if (sig <= 0 || sig >= NSIG) { + errno = EINVAL; + return -1; + } + if (old) + memset(old, 0, sizeof *old); + (void)act; + return 0; +} +int sigprocmask(int how, const sigset_t *set, sigset_t *old) { + (void)how, (void)set; + if (old) + *old = 0; + return 0; +} +int sigisemptyset(const sigset_t *set) { return *set == 0; } +int sigsuspend(const sigset_t *mask) { + (void)mask; + errno = ENOSYS; + return -1; +} + +/* ---- 資源・システム情報 ---- */ +int getrlimit(int resource, struct rlimit *rlim) { + (void)resource; + rlim->rlim_cur = rlim->rlim_max = RLIM_INFINITY; + return 0; +} +int setrlimit(int resource, const struct rlimit *rlim) { + (void)resource, (void)rlim; + return 0; +} +int getpagesize(void) { return 4096; } +long sysconf(int name) { + switch (name) { + case _SC_PAGESIZE: return 4096; + case _SC_CLK_TCK: return 100; + case _SC_OPEN_MAX: return PKCL_OPEN_MAX; + case _SC_ARG_MAX: return 32768; + case _SC_NPROCESSORS_ONLN: + case _SC_NPROCESSORS_CONF: return 1; + default: errno = EINVAL; return -1; + } +} +int uname(struct utsname *name) { + memset(name, 0, sizeof *name); + strcpy(name->sysname, "Foundation"); + strcpy(name->nodename, "cpt32"); + strcpy(name->release, "0"); + strcpy(name->version, "0"); + strcpy(name->machine, "src32"); + return 0; +} diff --git a/foundation/lib/pkcl/posix/setjmp.c b/foundation/lib/pkcl/posix/setjmp.c new file mode 100644 index 0000000..bd4c52e --- /dev/null +++ b/foundation/lib/pkcl/posix/setjmp.c @@ -0,0 +1,14 @@ +/* setjmp / longjmp はコンパイラ側の対応がまだない(doc/spec_pkcl.md §7 G7)。 + * setjmp は常に 0 を返し、longjmp は戻り先がないので終了する。 */ +#include +#include + +int setjmp(jmp_buf env) { + (void)env; + return 0; +} +void longjmp(jmp_buf env, int value) { + (void)env, (void)value; + fnd_con_write("pkcl: longjmp is not supported\n"); + fnd_exit(127); +} diff --git a/foundation/lib/pkcl/posix/term.c b/foundation/lib/pkcl/posix/term.c new file mode 100644 index 0000000..456f00d --- /dev/null +++ b/foundation/lib/pkcl/posix/term.c @@ -0,0 +1,121 @@ +/* 端末・ioctl・poll・pipe(doc/spec_pkcl.md §6.6)。端末は console の LINE / RAW モードに対応づける。 */ +#define _GNU_SOURCE 1 +#include +#include +#include +#include +#include +#include + +#include +#include + +static int is_console(int fd) { + fnd_handle h = pkcl_handle(fd); + fnd_stat_t st; + if (h && fnd_stream_stat(h, &st) == FND_OK && st.type == FND_TYPE_CONSOLE) + return 1; + if (h) + errno = ENOTTY; + return 0; +} + +/* LINE: 行編集と echo をコンソールが行う。RAW: 1 キーずつ返す。 */ +static int raw_mode; + +int tcgetattr(int fd, struct termios *t) { + if (!is_console(fd)) + return -1; + memset(t, 0, sizeof *t); + t->c_iflag = ICRNL | IXON; + t->c_oflag = OPOST | ONLCR; + t->c_cflag = CS8 | CREAD | CLOCAL | B38400; + t->c_lflag = ISIG | IEXTEN | (raw_mode ? 0 : (ICANON | ECHO | ECHOE | ECHOK)); + t->c_cc[VINTR] = 3; + t->c_cc[VQUIT] = 28; + t->c_cc[VERASE] = 127; + t->c_cc[VKILL] = 21; + t->c_cc[VEOF] = 4; + t->c_cc[VMIN] = 1; + t->c_cc[VSTART] = 17; + t->c_cc[VSTOP] = 19; + t->c_cc[VSUSP] = 26; + t->c_ispeed = t->c_ospeed = B38400; + return 0; +} + +int tcsetattr(int fd, int actions, const struct termios *t) { + (void)actions; + if (!is_console(fd)) + return -1; + int raw = !(t->c_lflag & ICANON); + if (raw != raw_mode) { + if (pkcl_fail(fnd_con_set_mode(raw ? FND_CON_RAW : FND_CON_LINE))) + return -1; + raw_mode = raw; + } + return 0; +} + +int tcflush(int fd, int queue) { + (void)queue; + return is_console(fd) ? 0 : -1; +} +int tcdrain(int fd) { return is_console(fd) ? 0 : -1; } +speed_t cfgetispeed(const struct termios *t) { return t->c_ispeed; } +speed_t cfgetospeed(const struct termios *t) { return t->c_ospeed; } +int cfsetispeed(struct termios *t, speed_t speed) { t->c_ispeed = speed; return 0; } +int cfsetospeed(struct termios *t, speed_t speed) { t->c_ospeed = speed; return 0; } +pid_t tcgetpgrp(int fd) { return is_console(fd) ? 1 : -1; } +int tcsetpgrp(int fd, pid_t pgrp) { (void)pgrp; return is_console(fd) ? 0 : -1; } + +int ttyname_r(int fd, char *buf, size_t size) { + if (!is_console(fd)) + return ENOTTY; + if (size < sizeof "/dev/console") + return ERANGE; + strcpy(buf, "/dev/console"); + return 0; +} + +int ioctl(int fd, unsigned long request, ...) { + __builtin_va_list ap; + __builtin_va_start(ap, request); + void *arg = __builtin_va_arg(ap, void *); + __builtin_va_end(ap); + if (request == TIOCGWINSZ) { + uint32_t cols, rows; + if (!is_console(fd)) + return -1; + if (pkcl_fail(fnd_con_size(&cols, &rows))) + return -1; + struct winsize *ws = arg; + ws->ws_col = (unsigned short)cols; + ws->ws_row = (unsigned short)rows; + ws->ws_xpixel = ws->ws_ypixel = 0; + return 0; + } + errno = is_console(fd) || !pkcl_handle(fd) ? EINVAL : ENOTTY; + return -1; +} + +/* 待てる fd は read が返ることを保証できないので、要求された入出力はすべて準備済みとして返す(§7 G9)。 */ +int poll(struct pollfd *fds, nfds_t count, int timeout) { + (void)timeout; + int ready = 0; + for (nfds_t i = 0; i < count; ++i) { + if (fds[i].fd < 0 || !pkcl_handle(fds[i].fd)) { + fds[i].revents = fds[i].fd < 0 ? 0 : POLLNVAL; + } else { + fds[i].revents = fds[i].events & (POLLIN | POLLOUT); + } + ready += fds[i].revents != 0; + } + return ready; +} + +int pipe(int fds[2]) { + (void)fds; + errno = ENOSYS; /* §7 G3: pipe サービスがまだない */ + return -1; +} diff --git a/tools/llvm-src32/backend.py b/tools/llvm-src32/backend.py index 853fabf..9177593 100644 --- a/tools/llvm-src32/backend.py +++ b/tools/llvm-src32/backend.py @@ -1293,6 +1293,76 @@ def integer_intrinsic(self, inst, name, values): self.copy(values[1], self.slots[inst]) c.label(done) return + if kind in ("fshl", "fshr") and bits == 64: + amount = values[2] + if amount.value_kind != llvm.ValueKind.constant_int: + self.fail("64-bit funnel shift needs a constant shift amount") + s = amount.get_constant_value() % 64 + if kind == "fshr": + s = (64 - s) % 64 + # fshl(A, B, s) = A << s | B >> (64 - s); words: R2/R4 high, R1/R3 low. + self.load(values[0], 1, 1) + self.load(values[0], 2, 0) + self.load(values[1], 3, 1) + self.load(values[1], 4, 0) + if s == 0: + c.move(5, 2 if kind == "fshl" else 4) + c.move(6, 1 if kind == "fshl" else 3) + else: + def shift(op, dst, src, n): + c.imm(9, n) + c.emit(f"{op} R{dst}, R{src}, R9") + # A << s + if s < 32: + shift("SLL", 5, 2, s) + shift("SRL", 7, 1, 32 - s) + c.emit("OR R5, R5, R7") + shift("SLL", 6, 1, s) + else: + shift("SLL", 5, 1, s - 32) + c.move(6, 0) + # B >> (64 - s) + t = 64 - s + if t < 32: + shift("SRL", 7, 3, t) + c.emit("OR R6, R6, R7") + shift("SLL", 7, 4, 32 - t) + c.emit("OR R6, R6, R7") + shift("SRL", 7, 4, t) + c.emit("OR R5, R5, R7") + else: + shift("SRL", 7, 4, t - 32) + c.emit("OR R6, R6, R7") + self.store(inst, 5, 0) + self.store(inst, 6, 1) + return + if kind in ("fshl", "fshr"): + if bits != 32: + self.fail(f"unsupported funnel shift width {bits}") + # fshl(a, b, s) = a << s | b >> (32 - s); fshr(a, b, s) = a << (32 - s) | b >> s; s is taken mod 32. + self.load(values[0], 1) + self.load(values[1], 2) + self.load(values[2], 3) + c.imm(9, 31) + c.emit("AND R3, R3, R9") + zero, done = c.fresh("funnel_zero"), c.fresh("funnel_done") + c.emit(f"BEQ R3, R0, {zero}") + c.imm(9, 32) + c.emit("SUB R4, R9, R3") # 32 - s + if kind == "fshl": + c.emit("SLL R1, R1, R3") + c.emit("SRL R2, R2, R4") + else: + c.emit("SLL R1, R1, R4") + c.emit("SRL R2, R2, R3") + c.emit("OR R1, R1, R2") + c.emit(f"JMP {done}") + c.label(zero) + if kind == "fshr": + c.move(1, 2) + c.label(done) + self.store(inst) + return self.load(values[0], 1, 1 if wide else 0) if wide: self.load(values[0], 2, 0) @@ -1400,6 +1470,73 @@ def intrinsic(self, inst, name, values): c.mem("ST", 0, 29, self.slots[inst] + 4) c.mem("STB", 4, 29, self.slots[inst] + self.layout.offset(inst.type, 1)) return + if name == "llvm.umul.with.overflow.i64": + # A = R2:R1, B = R4:R3. The product overflows 64 bits exactly when ahi and bhi are both + # non-zero, when either cross product ahi*blo, alo*bhi reaches 2^32, or when + # lo(ahi*blo) + lo(alo*bhi) + hi(alo*blo) carries out of the high word. + self.load(values[0], 1, 1) + self.load(values[0], 2, 0) + self.load(values[1], 3, 1) + self.load(values[1], 4, 0) + + def unsigned_high(dst, x, y, tmp): + c.emit(f"MULH R{dst}, R{x}, R{y}") + c.imm(9, 31) + c.emit(f"SRA R{tmp}, R{x}, R9") + c.emit(f"AND R{tmp}, R{tmp}, R{y}") + c.emit(f"ADD R{dst}, R{dst}, R{tmp}") + c.emit(f"SRA R{tmp}, R{y}, R9") + c.emit(f"AND R{tmp}, R{tmp}, R{x}") + c.emit(f"ADD R{dst}, R{dst}, R{tmp}") + + c.emit("SLTU R5, R0, R2") + c.emit("SLTU R6, R0, R4") + c.emit("AND R5, R5, R6") + for x, y in ((2, 3), (1, 4)): + unsigned_high(6, x, y, 7) + c.emit("SLTU R6, R0, R6") + c.emit("OR R5, R5, R6") + c.emit("MUL R6, R2, R3") + c.emit("MUL R7, R1, R4") + c.emit("ADD R6, R6, R7") + c.emit("SLTU R7, R6, R7") + c.emit("OR R5, R5, R7") + unsigned_high(7, 1, 3, 8) + c.emit("ADD R6, R6, R7") + c.emit("SLTU R6, R6, R7") + c.emit("OR R5, R5, R6") + slot = self.slots[inst] + c.mem("ST", 0, 29, slot + 8) + c.mem("STB", 5, 29, slot + self.layout.offset(inst.type, 1)) + unsigned_high(5, 1, 3, 6) + c.emit("MUL R2, R2, R3") + c.emit("MUL R4, R4, R1") + c.emit("ADD R2, R2, R4") + c.emit("ADD R2, R2, R5") + c.emit("MUL R1, R1, R3") + self.store(inst, 2, 0) + self.store(inst, 1, 1) + return + if name.startswith("llvm.uadd.sat.") and _bits(inst.type) <= 32: + bits = _bits(inst.type) + self.load(values[0], 1) + self.load(values[1], 3) + self.narrow(1, bits) + self.narrow(3, bits) + c.emit("ADD R1, R1, R3") # at most 33 bits only when bits == 32: detect through the carry + maximum = (1 << bits) - 1 + done = c.fresh("uadd_sat_done") + if bits == 32: + self.load(values[0], 4) + c.emit("SLTU R5, R1, R4") # wrapped: result < first operand + else: + c.imm(4, maximum) + c.emit("SLTU R5, R4, R1") # result > max + c.emit(f"BEQ R5, R0, {done}") + c.imm(1, maximum) + c.label(done) + self.store(inst) + return if name.startswith("llvm.usub.sat."): bits = _bits(inst.type) wide = bits > 32 @@ -1438,7 +1575,7 @@ def intrinsic(self, inst, name, values): if name.split(".")[1] in ("fabs", "copysign", "sqrt", "fma", "fmuladd"): self.float_intrinsic(inst, name, values) return - if name.split(".")[1] in ("smin", "smax", "umin", "umax", "abs", "bswap", "ctpop", "ctlz", "cttz"): + if name.split(".")[1] in ("smin", "smax", "umin", "umax", "abs", "bswap", "ctpop", "ctlz", "cttz", "fshl", "fshr"): self.integer_intrinsic(inst, name, values) return if name.startswith(("llvm.lifetime.", "llvm.dbg.")): @@ -1485,6 +1622,12 @@ def intrinsic(self, inst, name, values): c.emit("AND R1, R1, R2") self.store(inst) return + if name == "llvm.get.rounding": + c.imm(1, 1) # FLT_ROUNDS: to nearest, the only mode SoftFloat here uses + self.store(inst) + return + if name == "llvm.set.rounding": + return if name.startswith("llvm.expect."): self.copy(values[0], self.slots[inst]) return diff --git a/tools/llvm-src32/data.py b/tools/llvm-src32/data.py index 0c6e329..460138f 100644 --- a/tools/llvm-src32/data.py +++ b/tools/llvm-src32/data.py @@ -374,7 +374,7 @@ def resolve(self, text: str) -> int: def _resolve_expression(self, text: str) -> int: text = text.strip() - if text in {"null", "zeroinitializer", "false"}: + if text in {"null", "zeroinitializer", "false", "undef", "poison"}: return 0 if text == "true": return 1 diff --git a/tools/llvm-src32/driver.py b/tools/llvm-src32/driver.py index 783841f..2856139 100644 --- a/tools/llvm-src32/driver.py +++ b/tools/llvm-src32/driver.py @@ -55,7 +55,7 @@ def parser() -> argparse.ArgumentParser: help="process FILE before the source, like Clang's -include (application code only)") p.add_argument("-D", dest="defines", action="append", default=[]) opt = p.add_mutually_exclusive_group() - for level in range(3): + for level in ("0", "1", "2", "s", "z"): opt.add_argument(f"-O{level}", dest="optimization", action="store_const", const=f"-O{level}") p.set_defaults(optimization="-O0") p.add_argument("--origin", type=address, default=0) @@ -119,6 +119,9 @@ def find_clang(explicit: str | None) -> str: raise DriverError("Clang not found; install Clang and add it to PATH, or set --clang PATH / CLANG") +POSIX_LIBRARY_OPTIMIZATION = "-Os" + + def compile_c(clang: str, source: Path, output: Path, args, defines=(), runtime=False, depfile=None) -> None: # The bundled string loops are hot in every guest, including -O0 guests. # Keep the optional external Newlib profile's compilation policy separate. @@ -129,6 +132,9 @@ def compile_c(clang: str, source: Path, output: Path, args, defines=(), runtime= cxx = source.suffix.lower() in CXX_SUFFIXES newlib = getattr(args, "newlib", None) posix_library = bool(runtime and newlib and newlib.posix) + if posix_library and source.is_relative_to(newlib.root): + # The code size matters more than speed (Foundation runs an image from a 1 MiB buffer). + optimization = POSIX_LIBRARY_OPTIMIZATION if posix_library: # Newlib is written for GNU C and builds with the old implicit-declaration rules. defines = (*defines, *newlib_posix_defines()) diff --git a/tools/llvm-src32/newlib.py b/tools/llvm-src32/newlib.py index 5963cde..91634c9 100644 --- a/tools/llvm-src32/newlib.py +++ b/tools/llvm-src32/newlib.py @@ -3,6 +3,7 @@ import hashlib import json import os +import re from pathlib import Path import subprocess @@ -19,20 +20,41 @@ POSIX_DIRS = ("libc/argz", "libc/ctype", "libc/errno", "libc/locale", "libc/misc", "libc/reent", "libc/search", "libc/signal", "libc/stdio", "libc/stdlib", "libc/string", "libc/time", "libm/common", "libm/math") -POSIX_EXCLUDE = ("nano-", "mallocr", "malloc.c", "mlock", "nano-mallocr", "sbrkr", "mstats", "malloc_r", +POSIX_EXCLUDE = ("mallocr", "malloc.c", "mlock", "nano-mallocr", "sbrkr", "mstats", "malloc_r", "/stdlib/cxa_", "/stdlib/callocr.c", "/stdlib/cfreer.c", "/stdlib/freer.c", "/stdlib/malignr.c", "/stdlib/mallinfor.c", "/stdlib/malloptr.c", "/stdlib/mallstatsr.c", "/stdlib/msizer.c", - "/stdlib/pvallocr.c", "/stdlib/reallocr.c", "/stdlib/vallocr.c", "/stdlib/mallocr.c", "/misc/ffs", "fdopen64", "/stdio64/", + "/stdlib/pvallocr.c", "/stdlib/realloc.c", "/stdlib/calloc.c", "/stdlib/free.c", "/stdlib/valloc.c", "/stdlib/pvalloc.c", "/stdlib/memalign.c", "/stdlib/mallinfo.c", "/stdlib/malloc_usable_size.c", "/stdlib/reallocr.c", "/stdlib/vallocr.c", "/stdlib/mallocr.c", "/misc/ffs", "fdopen64", "/stdio64/", # the driver's own string.c supplies these (it compiles them at -O2) "/string/memcpy.c", "/string/memmove.c", "/string/memset.c", "/string/memcmp.c", "/string/strlen.c") # Files that Newlib compiles as variants of another file; compile only the first of a group. -POSIX_PRIORITY = ("stdio/vfprintf.c", "stdio/vfscanf.c", "stdio/svfprintf.c", "stdio/svfscanf.c") +POSIX_PRIORITY = ("stdio/nano-vfprintf.c", "stdio/nano-svfprintf.c", "stdio/nano-vfscanf.c", "stdio/nano-svfscanf.c", + "stdio/nano-vfprintf_i.c", "stdio/nano-vfscanf_i.c") POSIX_DEFINES = ("_COMPILING_NEWLIB", "_GNU_SOURCE") -CACHE_VERSION = "1" +CACHE_VERSION = "5" # 2: the library is compiled with -Os (it was -O0) REENT_ALLOCATORS = ("_malloc_r", "_free_r", "_realloc_r", "_calloc_r", "__malloc_lock", "__malloc_unlock") ALLOCATORS = ("malloc", "free", "calloc", "realloc", "malloc_usable_size") +_ALIAS = re.compile(r"^@([\w.$]+) = (?:[a-z_]+ )*alias (\w+) \(([^)]*)\), ptr @([\w.$]+)$", re.M) + + +def functions_for_aliases(text): + """The backend has no aliases or weak symbols. Newlib's nano printf and scanf make the integer-only entry points + (vfiprintf and the like) aliases of the full ones, so define them as forwarding functions.""" + def forward(match): + name, result, parameters, target = match.groups() + types = [t.strip() for t in parameters.split(",") if t.strip()] + names = ", ".join(f"{t} %a{i}" for i, t in enumerate(types)) + call = f"@{target}({names})" + if result == "void": + return f"define dso_local void @{name}({names}) {{\n tail call void {call}\n ret void\n}}" + return f"define dso_local {result} @{name}({names}) {{\n %r = tail call {result} {call}\n ret {result} %r\n}}" + text = _ALIAS.sub(forward, text) + # nano printf and scanf reach the floating-point code through weak references that the linker + # leaves null when it is absent. There are no weak symbols here: link the code in, always. + return re.sub(r"^(define|declare) (?:extern_weak|weak) ", r"\1 ", text, flags=re.M) + + class Newlib: def __init__(self, source, clang, heap_size, profile="portable", compile_ir=None, jobs=1, cache=None): self.root = Path(source).resolve() @@ -104,6 +126,7 @@ def build(path): if not ir.is_file(): try: self.compile_ir(path, ir) + ir.write_text(functions_for_aliases(ir.read_text(encoding="utf-8")), encoding="utf-8") except Exception as exc: # a file the toolchain cannot compile just provides nothing return path, None, str(exc).splitlines()[0] return path, ir, None diff --git a/tools/pkcl/busybox.config b/tools/pkcl/busybox.config index 8fcf6e9..cff89bb 100644 --- a/tools/pkcl/busybox.config +++ b/tools/pkcl/busybox.config @@ -26,3 +26,7 @@ CONFIG_LS=y CONFIG_SLEEP=y CONFIG_TEST=y CONFIG_PRINTF=y +CONFIG_SH_IS_HUSH=n +CONFIG_SH_IS_NONE=y +CONFIG_SHELL_HUSH=n +CONFIG_SH_IS_ASH=n diff --git a/tools/pkcl/busybox.py b/tools/pkcl/busybox.py index d177a8c..8146068 100644 --- a/tools/pkcl/busybox.py +++ b/tools/pkcl/busybox.py @@ -64,7 +64,7 @@ def configure(args, build: Path): lines.append(line) lines += [f"{k}={v}" for k, v in wanted.items() if k not in seen and v != "n"] (build / ".config").write_text("\n".join(lines) + "\n", encoding="utf-8") - run(["sh", "-c", f"yes n | {shlex.join(make)} oldconfig"], log=build / "oldconfig.log") + run(["sh", "-c", f"yes '' | timeout 300 {shlex.join(make)} oldconfig"], log=build / "oldconfig.log") # Hard dependencies can switch an option back off; say so rather than build something else. final = (build / ".config").read_text(encoding="utf-8") for key, value in wanted.items(): @@ -120,6 +120,18 @@ def driver_args(args): "--newlib-cache", str(args.cache), *(["--clang", args.clang] if args.clang else [])] +EMPTY_ASM = re.compile(r'^\s*(?:tail )?call void asm sideeffect "",.*\n', re.M) + + +def strip_empty_asm(ir: Path): + """BusyBox の barrier() が出す空のインラインアセンブリを消す(バックエンドは inline asm を持たず、 + SRC32 は 1 命令ずつ順に実行するのでコンパイラバリアは何もしなくてよい)。""" + text = ir.read_text() + stripped = EMPTY_ASM.sub("", text) + if stripped != text: + ir.write_text(stripped) + + def compile_all(args, build: Path, out: Path): out.mkdir(parents=True, exist_ok=True) units = target_sources(build) @@ -144,6 +156,7 @@ def compile_one(item): if result.returncode: failures[obj] = (result.stderr or result.stdout).strip() return None + strip_empty_asm(ir) return ir with ThreadPoolExecutor(max_workers=args.jobs) as pool: @@ -181,7 +194,7 @@ def main(argv=None): parser.add_argument("--config", type=Path, default=HERE / "busybox.config") parser.add_argument("--clang") parser.add_argument("--cache", type=Path, help="Newlib IR cache (default: tools/llvm-src32's)") - parser.add_argument("-O", dest="optimization", default="-O1", choices=["-O0", "-O1", "-O2"]) + parser.add_argument("-O", dest="optimization", default="-Os", choices=["-O0", "-O1", "-O2", "-Os", "-Oz"]) parser.add_argument("-j", "--jobs", type=int, default=os.cpu_count() or 1) parser.add_argument("--only", help="regular expression: compile only the matching objects (debugging)") parser.add_argument("--stage", choices=["configure", "compile", "link"], default="link") From c86c58526ecd7d45012295a3586e47796c7143ba Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 10:38:38 +0000 Subject: [PATCH 5/5] =?UTF-8?q?pkcl:=20BusyBox=20=E3=82=92=201=20MiB=20?= =?UTF-8?q?=E6=9C=AA=E6=BA=80=E3=81=AB=E3=81=97=E3=81=A6=20Foundation=20?= =?UTF-8?q?=E3=81=A7=E5=AE=9F=E8=A1=8C=E3=80=81=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=A8=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FqgVXKR79fR8rybAwBZ2fY --- doc/spec_pkcl.md | 80 ++++++++++++----- foundation/tests/pkcl/test_busybox.py | 89 +++++++++++++++++++ ...6-10-25-issue-170-busybox-on-foundation.md | 23 +++++ tools/llvm-src32/README.md | 4 +- tools/llvm-src32/newlib.py | 11 ++- tools/llvm-src32/runtime/newlib/stdio_i.c | 11 +++ tools/llvm-src32/test_toolchain.py | 69 ++++++++++++++ tools/pkcl/busybox.py | 6 +- tools/pkcl/busybox_stubs.c | 15 ++++ 9 files changed, 281 insertions(+), 27 deletions(-) create mode 100644 foundation/tests/pkcl/test_busybox.py create mode 100644 memory/2026-10-25-issue-170-busybox-on-foundation.md create mode 100644 tools/llvm-src32/runtime/newlib/stdio_i.c create mode 100644 tools/pkcl/busybox_stubs.c diff --git a/doc/spec_pkcl.md b/doc/spec_pkcl.md index 3952260..b40fa8f 100644 --- a/doc/spec_pkcl.md +++ b/doc/spec_pkcl.md @@ -34,9 +34,14 @@ fdio と同じく、pkcl の仕事は「ファイル記述子の表」と「POSI | 場所 | 内容 | |---|---| -| `foundation/include/pkcl/` | POSIX ヘッダ(`errno.h`、`fcntl.h`、`unistd.h`、`sys/types.h`、`sys/stat.h`…)と内部境界 `pkcl/pkcl.h` | +| `foundation/include/pkcl/` | 内部境界 `pkcl/pkcl.h` | +| `foundation/include/pkcl-nolibc/` | libc なしで使う POSIX ヘッダ(`errno.h`、`fcntl.h`、`unistd.h`、`sys/types.h`、`sys/stat.h`…) | +| `foundation/include/pkcl-posix/` | Newlib のヘッダに足す POSIX ヘッダ(`dirent.h`、`termios`、`poll.h`、`sys/socket.h`、`netdb.h`…)と、Newlib が隠す宣言(`lstat`、`nanosleep`、`getline`…)の補い | | `foundation/lib/pkcl/` | 実装(`errno.c`、`fd.c`、`fs.c`、`crt.c`) | -| `foundation/tests/pkcl/` | 実際の fs・console に対するゲストテスト | +| `foundation/lib/pkcl/newlib/` | Newlib の OS 層(`_open`、`_sbrk`、`_gettimeofday`…)を pkcl に結ぶアダプタ | +| `foundation/lib/pkcl/posix/` | libc ありで足す POSIX(`DIR`、端末・`ioctl`・`poll`、ソケットの失敗スタブ、プロセス・時刻・シグナルの縮退、ユーザー DB、`fnmatch`) | +| `tools/pkcl/` | BusyBox のビルド(`busybox.py`、`busybox.config`、`busybox_stubs.c`) | +| `foundation/tests/pkcl/` | 実際の fs・console に対するゲストテストと BusyBox のテスト | ### 3.2 ビルド @@ -48,7 +53,7 @@ python tools/llvm-src32/driver.py app.c foundation/lib/pkcl/*.c \ --entry pkcl_start --wax3 --import-wal root/lib/fnd.1.wal ... ``` -- `-I foundation/include/pkcl` を**先頭**に置き、libc のヘッダより先に pkcl のヘッダを見せます(libc 統合後は §9 で調整)。 +- libc なしなら `-I foundation/include/pkcl-nolibc` を先頭に置きます。Newlib を使うときは `--newlib-profile posix`(§9)と `-I foundation/include/pkcl-posix`、`-DPKCL_LIBC` を指定します。 - WAL `fnd` を import する WAX v3 が基本です。静的 libfnd(WAX v2)でも動きます。 ### 3.3 起動 @@ -221,12 +226,12 @@ BusyBox は GPL-2.0 なので、**リポジトリには含めません**。Newli | 必要なもの | 状態 | |---|---| | ファイル I/O、`stat`、`mkdir` 等 | 実装済み(§6.1〜6.3) | -| `DIR`(`ls`、`cp -r`、`rm -r`) | M1 | -| libc:`stdio`(`printf` 系、`fopen`、バッファ)、`strtol`、`qsort`、`getopt`、`regcomp`(`grep`、`sed`)、`locale` の最小 | M2(libc 統合) | -| `setjmp`/`longjmp`(ash が必須) | G7 | -| `posix_spawn`、`waitpid`(外部コマンド、`xargs`、`find -exec`) | M3 | -| `pipe`(シェルのパイプ、`popen`) | M4 | -| `termios`、`ioctl`、`signal`(対話シェル、`vi`、`top`) | M4 | +| `DIR`(`ls`、`cp -r`、`rm -r`) | 実装済み(`posix/dirent.c`) | +| libc:`stdio`(`printf` 系、`fopen`、バッファ)、`strtol`、`qsort`、`getopt`、`regcomp`(`grep`、`sed`)、`locale` の最小 | 実装済み(Newlib の posix プロファイル、§9) | +| `setjmp`/`longjmp`(ash が必須) | 未対応。`posix/setjmp.c` は `setjmp` が 0 を返し `longjmp` は終了する仮実装(G7) | +| `posix_spawn`、`waitpid`(外部コマンド、`xargs`、`find -exec`) | `execve` 系は「子を起動して終了を待ち、終了コードで自分も終わる」代理実装。`vfork`/`waitpid` は失敗(M2 の残り) | +| `pipe`(シェルのパイプ、`popen`) | `ENOSYS`(G3、M4) | +| `termios`、`ioctl`、`signal`(対話シェル、`vi`、`top`) | termios/`TIOCGWINSZ` は console の LINE/RAW に対応。`sigaction` は登録を受け付けるだけで配送されない。`poll` は要求をそのまま準備済みとして返す(G9) | | `getpwnam` 等 | BusyBox の `USE_BB_PWD_GRP` で自前実装を使い、`/etc/passwd` を読む | | 浮動小数点 | `llvm-src32` が SoftFloat で対応済み。BusyBox は `awk`、`dc` 以外で不要 | @@ -236,26 +241,52 @@ BusyBox の NOMMU 構成(`CONFIG_NOMMU`)と NOFORK/NOEXEC applet(`FEATUR ### 8.4 ビルド -`tools/pkcl/busybox.py`(予定)が、`--busybox-source PATH` の外部ソース、pkcl、libc を `tools/llvm-src32/driver.py` に渡して `busybox.wax` を作り、`/bin/busybox.wax` と各 applet 名のファイルをディスクに置きます(シンボリックリンクがないので、applet ごとに 1 つの小さな WAX で `busybox` を spawn し引数を足す、または shell 側のディスパッチ。`foundation/bin` の方式に合わせる)。WAX は 1 MiB の `FND_EXEC_IMAGE_MAX` に収まる必要があります(BusyBox は applet を絞って組む)。 +`tools/pkcl/busybox.py` が、`--busybox-source PATH` の外部ソース、`--newlib-source PATH` の Newlib、pkcl を `tools/llvm-src32/driver.py` に渡して `busybox.wax` を作ります。 + +``` +python3 tools/pkcl/busybox.py --busybox-source /path/to/busybox-1.36.1 \ + --newlib-source /path/to/newlib-4.4.0.20231231 --fnd-wal root/lib/fnd.1.wal --out build/busybox +``` + +1. `allnoconfig` に `tools/pkcl/busybox.config` の差分を重ね、ホストの gcc で `make V=1` して生成ヘッダとコンパイルコマンド(`-I`/`-D`/`-include`)を集める。 +2. 各翻訳単位を `driver.py -c` で LLVM IR にする(`-Os`、`gnu11`)。`-D__clang_major__=0` で Clang 向けのインラインアセンブリの分岐を避け、`-DBB_GLOBAL_CONST=` で `ptr_to_globals` を rodata に置かせない(U モードで書込みフォルトになる)。バリアの空 asm は IR から消す。 +3. 全部を一度にリンクして `busybox.wax`(WAX v3、`fnd` WAL を import)を作る。 + +`/bin/busybox.wax` を置いて `busybox echo hi` のように呼びます(シンボリックリンクがないので applet 名のファイルは作らない。必要なら小さな WAX か shell 側のディスパッチ)。WAX は 1 MiB の `FND_EXEC_IMAGE_MAX` に収まる必要があり、シェルのヒープ(スロット 4 は 2 MiB)に丸ごと読み込まれるので、**サイズが applet の数を決める**。現在の構成は 1.04 MB です。サイズの内訳と抑えている手段は §9 を参照。 + +**設定が選ぶもの**:`CONFIG_NOMMU=y`、`CONFIG_LFS=n`(`off_t` は 32 bit)、`CONFIG_STATIC=y`、シェル(ash/hush)なし。applet は `cat echo true false pwd basename dirname head wc mkdir rmdir rm cp mv ls sleep test printf`。applet を足すにはサイズを確認すること(`tools/pkcl/busybox.py` のあとで `busybox.wax` が 1 MiB 未満であること。テストも確認する)。 ## 9. libc の方針 -`tools/llvm-src32` の Newlib 統合は、現状は文字列・整数・`qsort`・nano allocator だけで、`printf`/stdio/ファイル I/O は未対応です(`examples/newlib-c/README.md`)。pkcl は libc を持たず、次の順で進めます。 +pkcl は libc を持たず、Newlib(`--newlib-source` の外部ソース。同梱しない)を使います。`tools/llvm-src32` の Newlib 統合には 2 つのプロファイルがあります。 + +- `portable`(既定):文字列・整数・`qsort`・nano allocator だけを、必要なものだけコンパイルする。 +- `posix`(`--newlib-profile posix`):`argz ctype errno locale misc reent search signal stdio stdlib string time` と `libm` のほぼ全部を一度 IR にして `--newlib-cache` に置き、**未解決シンボルの索引**から必要な翻訳単位だけをリンクする。OS 層(`posix/`、`sys/`、`machine/`)は pkcl(`foundation/lib/pkcl/newlib/syscalls.c`)が `_open` や `_sbrk` などを pkcl の POSIX 関数と `fnd_sys_mem_grow` に結ぶ。 + +posix プロファイルの判断(サイズが 1 MiB の制約に直結するため): -1. **M2**:Newlib の stdio を載せられるか調査する。必要なのは `_open_r`/`_read_r`/`_write_r`/`_close_r`/`_lseek_r`/`_fstat_r`/`_isatty_r`/`_sbrk_r` などの reent 層で、これは pkcl の関数へ 1 対 1 の薄いアダプタ(`pkcl/newlib_syscalls.c`、予定)にできます。障害になりそうな `llvm-src32` の未対応事項(`_impure_ptr`、TLS、`setjmp`、atomic)は Issue にします。 -2. 載せられない場合の代替:`printf` 系だけ自前の最小実装(`sol/cpt32` の C 版や `sdk/cpt32/src/print.c` 相当)を pkcl の `libc-lite` として持つ。 -3. pkcl のヘッダは libc のヘッダと衝突しないよう、`errno`、`fcntl`、`unistd`、`sys/stat`、`sys/types` だけを持ち、`stdio.h` などは libc のものを使います。 +| 判断 | 理由 | +|---|---| +| ライブラリは `-Os` | `-O0` では BusyBox が 1.8 MB | +| nano の scanf を使い、printf は full | nano の printf は `%lld` に対応しない(`ll` を解釈せず "ld" と出力する)。BusyBox の `printf` が必要とする | +| full の `_svfprintf_r`(文字列用)と整数専用の `_vfiprintf_r`/`_svfiprintf_r` を、`_vfprintf_r` への転送関数にする(`runtime/newlib/stdio_i.c`) | 同じ整形器が 3 つ入って約 130 KB 増える。文字列の FILE は fd を持たないので full の整形器がそのまま書ける | +| IR のエイリアスを転送関数に、`weak`/`extern_weak` を強いシンボルに書き換える | バックエンドにエイリアスと弱いシンボルがない。nano scanf の `_scanf_float` を常にリンクするため、BusyBox は `tools/pkcl/busybox_stubs.c` の失敗スタブで置き換えて strtod 一式(約 60 KB)を外す | +| `setjmp`/`longjmp` は仮実装 | G7 | + +バックエンドに足した命令(最適化したコードが出す):`llvm.fshl`/`llvm.fshr`(i32 は変数のシフト量、i64 は定数のシフト量)、`llvm.umul.with.overflow.i64`、`llvm.uadd.sat`(32 bit 以下)、`llvm.get.rounding`/`llvm.set.rounding`、グローバル初期値の `undef`/`poison`。Clang は `-std=gnu11` で、`asm` キーワードが使えます(インラインアセンブリそのものは未対応)。 + +pkcl のヘッダは libc なしの `pkcl-nolibc`(`errno`、`fcntl`、`unistd`、`sys/stat`、`sys/types`)と、Newlib のヘッダの足りない分を補う `pkcl-posix` に分かれます。`pkcl-posix` のラッパーは `#include_next` で Newlib のヘッダを呼び、隠されている宣言を足します。 ## 10. マイルストーンと Issue | マイルストーン | 内容 | 状態 | |---|---|---| -| M0 | 設計、ヘッダ、fd I/O、パス操作、errno、起動コード、ゲストテスト | **この PR** | -| M1 | ディレクトリ(`DIR`)、`O_DIRECTORY`、`statvfs`、`chown`、`dup3`、`pread`/`pwrite` | 予定 | -| M2 | libc 統合(Newlib stdio を調査)、`posix_spawn`・`waitpid`・代理 `execve` | 予定 | -| M3 | 環境変数(G2)、`getpid`(G1)、時刻、`sleep` | 予定 | -| M4 | pipe サービス(G3)、`poll`、シグナル、termios | 予定 | -| M5 | BusyBox のビルド統合(`--busybox-source`)、最初の applet、ash/hush の調査、`setjmp`(G7) | 予定 | +| M0 | 設計、ヘッダ、fd I/O、パス操作、errno、起動コード、ゲストテスト | 完了 | +| M1 | ディレクトリ(`DIR`)、`O_DIRECTORY`、`statvfs`、`chown`、`dup3`、`pread`/`pwrite` | `DIR`・`chown` 縮退は完了。残りは予定 | +| M2 | libc 統合(Newlib の posix プロファイル)、代理 `execve` | 完了。`posix_spawn`・`waitpid` は予定 | +| M3 | 環境変数(G2)、`getpid`(G1)、時刻、`sleep` | 時刻・`sleep` は完了。G1・G2 は予定(`getpid` は 1 を返す仮実装) | +| M4 | pipe サービス(G3)、`poll`、シグナル、termios | termios は完了。他は予定 | +| M5 | BusyBox のビルド統合(`--busybox-source`)、最初の applet、ash/hush の調査、`setjmp`(G7) | ビルド統合と最初の applet は完了。ash/hush と `setjmp` は予定 | | M6 | 64 bit オフセット(G8)、`utime`(G5)、inode(G4) | 予定 | 各 Issue は GitHub Issues にあります。GitHub のマイルストーンは分野の分類(Foundation OS)で、上の M は Issue タイトルの接頭辞に書きます。 @@ -268,6 +299,15 @@ BusyBox の NOMMU 構成(`CONFIG_NOMMU`)と NOFORK/NOEXEC applet(`FEATUR python3 -m unittest discover -s foundation/tests/pkcl -v ``` +BusyBox のテスト(`test_busybox.py`)は GPL のソースを置かないので、環境変数を渡したときだけ走ります。 + +``` +SRC32_BUSYBOX_SOURCE=/path/to/busybox-1.36.1 SRC32_NEWLIB_SOURCE=/path/to/newlib-4.4.0.20231231 \ + python3 -m unittest discover -s foundation/tests/pkcl -k busybox -v +``` + +BusyBox のビルドが数分、起動が 1 MiB のイメージの読み込みで遅いので、全体で数分かかります。 + - Clang と `llvmlite` の LLVM のメジャー版を合わせてください(`CLANG` で指定)。`src32_testbench` は release ビルドを使います(`SRC32_TESTBENCH`)。 - ゲストは `--entry pkcl_start` で `main(argc, argv)` を持ち、失敗は失敗した行番号で終了します。テストは `pkcl guest passed` の出力と `status=0` を確認します。 diff --git a/foundation/tests/pkcl/test_busybox.py b/foundation/tests/pkcl/test_busybox.py new file mode 100644 index 0000000..e99a31b --- /dev/null +++ b/foundation/tests/pkcl/test_busybox.py @@ -0,0 +1,89 @@ +"""BusyBox(外部ソース)を pkcl と Newlib で組み、Foundation 上で applet を動かす。 + +BusyBox は GPL-2.0 でリポジトリに含めないので、ソースの場所を環境変数で渡したときだけ走る。 + + SRC32_BUSYBOX_SOURCE=/path/to/busybox-1.36.1 SRC32_NEWLIB_SOURCE=/path/to/newlib-4.4.0.20231231 \\ + python -m unittest discover -s foundation/tests/pkcl -k busybox -v + +CLANG と SRC32_TESTBENCH で実行ファイルを指定できます。BusyBox のビルドは数分かかります。 +""" +import os +from pathlib import Path +import subprocess +import sys +import unittest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] +sys.path.insert(0, str(HERE.parent / 'shell')) +import test_shell # noqa: E402 + +BUSYBOX = os.environ.get('SRC32_BUSYBOX_SOURCE') +NEWLIB = os.environ.get('SRC32_NEWLIB_SOURCE') + + +@unittest.skipUnless(BUSYBOX and NEWLIB, 'SRC32_BUSYBOX_SOURCE と SRC32_NEWLIB_SOURCE が必要') +class BusyboxTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + # 1 MiB の busybox.wax は読み込みだけでも時間がかかるので、既定の 8 秒では足りない。 + test_shell.STOP_TICKS = 2000 + test_shell.ShellTests.setUpClass.__func__(cls) + out = cls.work / 'busybox' + command = [sys.executable, str(ROOT / 'tools/pkcl/busybox.py'), '--busybox-source', BUSYBOX, + '--newlib-source', NEWLIB, '--fnd-wal', str(cls.work / 'root/lib/fnd.1.wal'), '--out', str(out)] + if os.environ.get('CLANG'): + command += ['--clang', os.environ['CLANG']] + result = subprocess.run(command, capture_output=True, text=True, timeout=3000) + if result.returncode: + raise RuntimeError(result.stdout + result.stderr) + wax = (out / 'busybox.wax').read_bytes() + # 実行形式は 1 MiB(FND_EXEC_IMAGE_MAX)まで。収まらないと proc_exec が FND_E_LIMIT になる。 + assert len(wax) < 0x100000, f'busybox.wax is {len(wax)} bytes' + volume = test_shell.fs2.Volume(bytearray(cls.disk)) + volume.put('/bin/busybox.wax', wax, mode=0x3D) + cls.disk = bytes(volume.img) + + @classmethod + def tearDownClass(cls): + test_shell.ShellTests.tearDownClass.__func__(cls) + + def run_commands(self, commands, frames): + run = self.work / 'run' + run.mkdir(exist_ok=True) + (run / 'disk.img').write_bytes(self.disk) + (run / 'config.yaml').write_text( + 'memory:\n ram_mib: 16\n vram_mib: 4\n' + 'disk:\n - slot: 0\n type: hdd\n image: disk.img\n read_only: false\n', encoding='utf-8') + command = [str(self.harness), str(self.image), '42', '--frames', str(frames), '--dump-pcg', + '--cpu', os.environ.get('FOUNDATION_TEST_CPU', 'cached')] + for index, text in enumerate(commands): + command += ['--type', f'{30 + index * 70}={text}\\n'] + result = subprocess.run(command, cwd=run, capture_output=True, text=True, + encoding='utf-8', errors='replace', timeout=1800) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + return result.stdout + + def test_applets(self): + screen = self.run_commands([ + 'busybox echo hello', + 'busybox printf %d-%s 42 abc', + 'busybox printf %5.2f 3.14159', + 'busybox mkdir /tmp/d', + 'busybox basename /a/b.txt .txt', + 'busybox dirname /a/b/c', + 'busybox test -d /tmp/d', + 'busybox cat /nonexistent', + 'busybox ls /tmp', + ], 2000) + for expected in ('hello', '42-abc', ' 3.14', '/a/b', "cat: can't open '/nonexistent': No such file or directory"): + self.assertIn(expected, screen) + + def test_help_lists_applets(self): + screen = self.run_commands(['busybox --help'], 1800) + self.assertIn('BusyBox v', screen) + self.assertIn('basename, cat, cp', screen) + + +if __name__ == '__main__': + unittest.main() diff --git a/memory/2026-10-25-issue-170-busybox-on-foundation.md b/memory/2026-10-25-issue-170-busybox-on-foundation.md new file mode 100644 index 0000000..8396e1b --- /dev/null +++ b/memory/2026-10-25-issue-170-busybox-on-foundation.md @@ -0,0 +1,23 @@ +# BusyBox を Foundation 上で動かす(pkcl M2〜M5) + +BusyBox 1.36.1 を pkcl と Newlib(どちらも外部ソース。GPL の BusyBox はリポジトリに入れない)で組み、Foundation で `busybox echo/printf/ls/cat/mkdir/cp/wc…` が動くところまで来た。契約は `doc/spec_pkcl.md` §8〜§9。 + +## 検証 + +- ビルド: `python3 tools/pkcl/busybox.py --busybox-source … --newlib-source … --fnd-wal root/lib/fnd.1.wal --out DIR`(Clang は llvmlite と同じメジャー版)。 +- テスト: `SRC32_BUSYBOX_SOURCE=… SRC32_NEWLIB_SOURCE=… python3 -m unittest discover -s foundation/tests/pkcl -k busybox`(環境変数がなければ skip)。バックエンドの命令は `tools/llvm-src32` の `test_toolchain.ToolchainTests.test_funnel_shift_wide_multiply_overflow_and_unsigned_saturating_add`。 + +## 落とし穴 + +- **実行形式は 1 MiB まで、実質はシェルのヒープ(スロット 4 は 2 MiB)に収まる必要がある。** `FND_EXEC_IMAGE_MAX` を 4 MiB にしても `busybox: out of memory (-5)` になる。サイズは applet の数を決める。1.83 MB からは次で 1.04 MB にした: Newlib を `-O0` から `-Os`(posix プロファイルが `-O0` でコンパイルしていた)、hush を外す、printf/scanf の重複整形器(`_svfprintf_r`、`_vfiprintf_r`)を転送関数にする、`_scanf_float` を失敗スタブにして strtod 一式を外す。applet を足したら必ず `busybox.wax` のサイズを見る。 +- **nano の printf は `%lld` に対応しない**(`ll` を解釈せず "ld" と出る)。BusyBox の `printf` が壊れるので printf は full、scanf は nano。nano はエイリアスと弱いシンボル(`_printf_float`/`_scanf_float`)を使うが、バックエンドにはどちらもない。キャッシュする IR を書き換えて対処している(`newlib.py` の `functions_for_aliases`)。 +- **`ptr_to_globals` は `-DBB_GLOBAL_CONST=` が必須。** 既定では `const` で rodata に置かれ、`SET_PTR_TO_GLOBALS` の書込みが U モードのフォルト(`fault 80060002`)になる。 +- **`-D__clang_major__=0`**: libbb.h の Clang 向け分岐が `asm volatile` を使うため。`barrier()` の空 asm は IR から消す(バックエンドは inline asm 非対応)。 +- BusyBox の `make oldconfig` に `yes n` を渡すと、選択肢(choice)の質問で無限ループして出力がメモリを食い、プロセスが kill される。`yes ''` と `timeout` を使う。`allnoconfig` は `SH_IS_ASH=y` が既定で、NOMMU では無効のため hush に落ちる。`SH_IS_ASH=n SH_IS_HUSH=n SH_IS_NONE=y SHELL_HUSH=n` を指定する。 +- 最適化したコードは `llvm.fshl/fshr`、`umul.with.overflow.i64`、`uadd.sat`、`get.rounding` などの命令を出す。足りない命令が出たら `backend.py` の `intrinsic` に足し、上のテストに入れる。i64 の `fshl/fshr` は定数のシフト量だけ対応。 +- Foundation のテストで長い実行をするときは、`test_shell.STOP_TICKS` を伸ばし(既定 800 = 660 フレーム)、`--frames` もそれに合わせる(約 0.83 フレーム/tick)。 +- `pkcl-posix/` のラッパーヘッダは `#include_next` で Newlib のヘッダを呼ぶ。Newlib は `lstat`、`nanosleep`、`getline`、`mknod`、`SA_RESTART`、`GLOB_NOMATCH` などを隠す/持たない。足りないときはここに足す。 + +## まだないもの + +`fork`/`vfork`(ENOSYS)、`waitpid`(ECHILD)、`pipe`(ENOSYS、G3)、`setjmp`/`longjmp`(仮実装、G7)、環境変数(G2)、pid(G1)、シグナルの配送、`glob` のディレクトリ走査。ash/hush が動くにはこれらが要る。`cp` は大きいファイルで `No space left on device`(テスト用ディスクの /tmp が小さい。pkcl の問題ではない)。 diff --git a/tools/llvm-src32/README.md b/tools/llvm-src32/README.md index cd7d4e0..87aa8f5 100644 --- a/tools/llvm-src32/README.md +++ b/tools/llvm-src32/README.md @@ -193,7 +193,9 @@ SIMD・ベクトル、`half`/`fp128`、atomic、例外、inline asm、TLS、alia LLVM IR 全仕様/C11 全仕様を実装したという意味ではありません。 既定ではstdio、malloc、ファイル、OS API は付属しません。標準ヘッダーも `stddef.h/stdint.h/stdbool.h/stdarg.h/limits.h/string.h` の freestanding 用です。 -`--newlib-source PATH`(CMake: `NEWLIB_SOURCE PATH`)を明示すると、別途用意したNewlibのヘッダーとportable Cソースを使用し、文字列・整数・ソート・nano allocatorを利用できます。Newlib本体は同梱・自動取得しません。対応API、ヒープ設定、取得方法、配布上の注意は[外部Newlibサンプル](../../examples/newlib-c/README.md)を参照してください。printf/stdio、ファイルI/O、locale、libmなどのNewlib全体は未対応です。 +`--newlib-source PATH`(CMake: `NEWLIB_SOURCE PATH`)を明示すると、別途用意したNewlibのヘッダーとportable Cソースを使用し、文字列・整数・ソート・nano allocatorを利用できます。Newlib本体は同梱・自動取得しません。対応API、ヒープ設定、取得方法、配布上の注意は[外部Newlibサンプル](../../examples/newlib-c/README.md)を参照してください。既定の `portable` プロファイルでは printf/stdio、ファイルI/O、locale、libmなどのNewlib全体は使えません。 + +`--newlib-profile posix`(`--newlib-cache DIR` でIRのキャッシュ先を指定)は、`argz ctype errno locale misc reent search signal stdio stdlib string time` と `libm` を一度IRにしてキャッシュし、未解決シンボルの索引から必要な翻訳単位だけをリンクします。ライブラリは `-Os`、printfはfull版(`%lld`対応)、scanfはnano版です。OS層(`_open`、`_sbrk`など)は利用側が用意します(Foundationでは pkcl、[doc/spec_pkcl.md](../../doc/spec_pkcl.md) §9)。バックエンドにエイリアスと弱いシンボルがないため、キャッシュするIRでエイリアスは転送関数に、弱いシンボルは強いシンボルに書き換えます。ユーザーコードは `-std=gnu11` でコンパイルされます。`-Os`/`-Oz` も指定できます。 追加の API はCで実装して一緒にリンクするか、同じ ABI を守る SRC32 ランタイムとして用意してください。 外部 C ライブラリの組み込み例は [`Berkeley SoftFloat デモ`](../../examples/softfloat-c/README.md)。Release 3e のソースを同梱し、整数演算で binary32 を実装するライブラリを `-O2` でリンクします(C の `float` 演算そのものはコンパイラが同じ SoftFloat へ変換するので、このデモのようにビット列を直接扱う必要はありません)。 既存 sol / csc の関数呼出 ABI とはバイナリ互換ではありません。 diff --git a/tools/llvm-src32/newlib.py b/tools/llvm-src32/newlib.py index 91634c9..7423568 100644 --- a/tools/llvm-src32/newlib.py +++ b/tools/llvm-src32/newlib.py @@ -20,17 +20,17 @@ POSIX_DIRS = ("libc/argz", "libc/ctype", "libc/errno", "libc/locale", "libc/misc", "libc/reent", "libc/search", "libc/signal", "libc/stdio", "libc/stdlib", "libc/string", "libc/time", "libm/common", "libm/math") -POSIX_EXCLUDE = ("mallocr", "malloc.c", "mlock", "nano-mallocr", "sbrkr", "mstats", "malloc_r", +POSIX_EXCLUDE = ("nano-vfprintf", "nano-svfprintf", "/stdio/vfiprintf.c", "/stdio/svfiprintf.c", "/stdio/svfprintf.c", "mallocr", "malloc.c", "mlock", "nano-mallocr", "sbrkr", "mstats", "malloc_r", "/stdlib/cxa_", "/stdlib/callocr.c", "/stdlib/cfreer.c", "/stdlib/freer.c", "/stdlib/malignr.c", "/stdlib/mallinfor.c", "/stdlib/malloptr.c", "/stdlib/mallstatsr.c", "/stdlib/msizer.c", "/stdlib/pvallocr.c", "/stdlib/realloc.c", "/stdlib/calloc.c", "/stdlib/free.c", "/stdlib/valloc.c", "/stdlib/pvalloc.c", "/stdlib/memalign.c", "/stdlib/mallinfo.c", "/stdlib/malloc_usable_size.c", "/stdlib/reallocr.c", "/stdlib/vallocr.c", "/stdlib/mallocr.c", "/misc/ffs", "fdopen64", "/stdio64/", # the driver's own string.c supplies these (it compiles them at -O2) "/string/memcpy.c", "/string/memmove.c", "/string/memset.c", "/string/memcmp.c", "/string/strlen.c") # Files that Newlib compiles as variants of another file; compile only the first of a group. -POSIX_PRIORITY = ("stdio/nano-vfprintf.c", "stdio/nano-svfprintf.c", "stdio/nano-vfscanf.c", "stdio/nano-svfscanf.c", - "stdio/nano-vfprintf_i.c", "stdio/nano-vfscanf_i.c") +POSIX_PRIORITY = ("stdio/nano-vfscanf.c", "stdio/nano-svfscanf.c", "stdio/nano-vfscanf_i.c") POSIX_DEFINES = ("_COMPILING_NEWLIB", "_GNU_SOURCE") -CACHE_VERSION = "5" # 2: the library is compiled with -Os (it was -O0) +CACHE_VERSION = "7" # 2: the library is compiled with -Os (it was -O0) +INTEGER_PRINTF = ("_vfiprintf_r", "vfiprintf", "_svfiprintf_r", "_svfprintf_r") REENT_ALLOCATORS = ("_malloc_r", "_free_r", "_realloc_r", "_calloc_r", "__malloc_lock", "__malloc_unlock") ALLOCATORS = ("malloc", "free", "calloc", "realloc", "malloc_usable_size") @@ -195,4 +195,7 @@ def port_pending(self, missing): if self.posix and missing & set(REENT_ALLOCATORS) and "reent-alloc" not in self.loaded: self.loaded.add("reent-alloc") result.append(("reent-alloc", PORT / "reent_alloc.c", [])) + if self.posix and missing & set(INTEGER_PRINTF) and "stdio-i" not in self.loaded: + self.loaded.add("stdio-i") + result.append(("stdio-i", PORT / "stdio_i.c", [])) return result diff --git a/tools/llvm-src32/runtime/newlib/stdio_i.c b/tools/llvm-src32/runtime/newlib/stdio_i.c new file mode 100644 index 0000000..8d9fb04 --- /dev/null +++ b/tools/llvm-src32/runtime/newlib/stdio_i.c @@ -0,0 +1,11 @@ +/* The posix profile (doc/spec_pkcl.md §9) runs one formatter. Newlib keeps a second copy for strings + * (_svfprintf_r, 63 KB) and a third for integers only (_vfiprintf_r and _svfiprintf_r, 32 KB each). + * A string FILE has no file descriptor, so the full formatter prints into it directly. */ +#include +#include +#include + +int _vfiprintf_r(struct _reent *r, FILE *fp, const char *fmt, va_list ap) { return _vfprintf_r(r, fp, fmt, ap); } +int vfiprintf(FILE *fp, const char *fmt, va_list ap) { return vfprintf(fp, fmt, ap); } +int _svfiprintf_r(struct _reent *r, FILE *fp, const char *fmt, va_list ap) { return _svfprintf_r(r, fp, fmt, ap); } +int _svfprintf_r(struct _reent *r, FILE *fp, const char *fmt, va_list ap) { return _vfprintf_r(r, fp, fmt, ap); } diff --git a/tools/llvm-src32/test_toolchain.py b/tools/llvm-src32/test_toolchain.py index df73fce..5922cdd 100644 --- a/tools/llvm-src32/test_toolchain.py +++ b/tools/llvm-src32/test_toolchain.py @@ -254,6 +254,75 @@ def test_unsigned_saturating_subtraction(self): for cpu in ('scalar', 'pipeline'): self.execute(image, 42, cpu) + def test_funnel_shift_wide_multiply_overflow_and_unsigned_saturating_add(self): + # Intrinsics that optimized Newlib and BusyBox code reaches (doc/spec_pkcl.md §8). + declarations = ['declare i32 @llvm.fshl.i32(i32, i32, i32)', 'declare i32 @llvm.fshr.i32(i32, i32, i32)', + 'declare i64 @llvm.fshl.i64(i64, i64, i64)', 'declare i64 @llvm.fshr.i64(i64, i64, i64)', + 'declare { i64, i1 } @llvm.umul.with.overflow.i64(i64, i64)', + 'declare i32 @llvm.uadd.sat.i32(i32, i32)', 'declare i8 @llvm.uadd.sat.i8(i8, i8)'] + functions = ["""define i32 @fshl32(i32 %a, i32 %b, i32 %s) { + %r = call i32 @llvm.fshl.i32(i32 %a, i32 %b, i32 %s) + ret i32 %r +}""", """define i32 @fshr32(i32 %a, i32 %b, i32 %s) { + %r = call i32 @llvm.fshr.i32(i32 %a, i32 %b, i32 %s) + ret i32 %r +}""", """define i32 @uadd32(i32 %a, i32 %b) { + %r = call i32 @llvm.uadd.sat.i32(i32 %a, i32 %b) + ret i32 %r +}""", """define i8 @uadd8(i8 %a, i8 %b) { + %r = call i8 @llvm.uadd.sat.i8(i8 %a, i8 %b) + ret i8 %r +}""", """define i64 @mulo(i64 %a, i64 %b, ptr %flag) { + %r = call { i64, i1 } @llvm.umul.with.overflow.i64(i64 %a, i64 %b) + %o = extractvalue { i64, i1 } %r, 1 + %z = zext i1 %o to i8 + store i8 %z, ptr %flag + %v = extractvalue { i64, i1 } %r, 0 + ret i64 %v +}"""] + checks, index = [], 0 + + def check(call, ty, expected): + nonlocal index + checks.extend([f' %r{index} = {call}', f' %ok{index} = icmp eq {ty} %r{index}, {expected}', + f' %all{index} = and i1 %ok{index}, ' + ('true' if index == 0 else f'%all{index - 1}')]) + index += 1 + + mask32, mask64 = 0xffffffff, 0xffffffffffffffff + for a, b in ((0x12345678, 0x9abcdef0), (0x80000001, 0x7fffffff)): + for s in (0, 1, 7, 31, 32, 33): + n = s % 32 + left = ((a << n) | (b >> (32 - n))) & mask32 if n else a + right = ((a << (32 - n)) | (b >> n)) & mask32 if n else b + check(f'call i32 @fshl32(i32 {a}, i32 {b}, i32 {s})', 'i32', left) + check(f'call i32 @fshr32(i32 {a}, i32 {b}, i32 {s})', 'i32', right) + for a, b in ((0, 0), (1, 2), (mask32, 1), (mask32, mask32), (0x7fffffff, 0x7fffffff), (0x80000000, 0x80000000)): + check(f'call i32 @uadd32(i32 {a}, i32 {b})', 'i32', min(a + b, mask32)) + for a, b in ((0, 0), (100, 100), (200, 55), (200, 56), (255, 255)): + check(f'call i8 @uadd8(i8 {a}, i8 {b})', 'i8', min(a + b, 255)) + # The 64-bit funnel shift needs a constant amount. + wide = 0x0123456789abcdef, 0xfedcba9876543210 + for s in (1, 5, 31, 32, 33, 63): + left = ((wide[0] << s) | (wide[1] >> (64 - s))) & mask64 + right = ((wide[0] << (64 - s)) | (wide[1] >> s)) & mask64 + check(f'call i64 @llvm.fshl.i64(i64 {wide[0]}, i64 {wide[1]}, i64 {s})', 'i64', left) + check(f'call i64 @llvm.fshr.i64(i64 {wide[0]}, i64 {wide[1]}, i64 {s})', 'i64', right) + for a, b in ((0, 0), (1, 1), (0xffffffff, 0xffffffff), (0x100000000, 0xffffffff), (0x100000000, 0x100000000), + (0x10000, 0x100000000), (0xffffffff, 0x100000001), (0x7fffffffffffffff, 2), (mask64, 1), + (1 << 63, 2), (0x123456789, 0x1fffffff), (3, 0x5555555555555555), (3, 0x5555555555555556)): + product = a * b + check(f'call i64 @mulo(i64 {a}, i64 {b}, ptr %flag)', 'i64', product & mask64) + checks.extend([f' %fl{index} = load i8, ptr %flag', f' %fok{index} = icmp eq i8 %fl{index}, {int(product > mask64)}', + f' %all{index} = and i1 %fok{index}, %all{index - 1}']) + index += 1 + source = self.ir('\n'.join(declarations + functions) + '\n' + + 'define i32 @main() {\n %flag = alloca i8\n' + '\n'.join(checks) + + f'\n %result = select i1 %all{index - 1}, i32 42, i32 1\n ret i32 %result\n}}\n') + image = self.work / 'intrinsics.wax' + self.driver(source, '-o', image) + for cpu in ('scalar', 'pipeline'): + self.execute(image, 42, cpu) + def test_softfloat_c_example(self): example = ROOT / 'examples/softfloat-c' library = example / 'vendor/softfloat/source' diff --git a/tools/pkcl/busybox.py b/tools/pkcl/busybox.py index 8146068..e530fd5 100644 --- a/tools/pkcl/busybox.py +++ b/tools/pkcl/busybox.py @@ -117,7 +117,8 @@ def target_sources(build: Path): def driver_args(args): return ["--newlib-source", str(args.newlib_source), "--newlib-profile", "posix", - "--newlib-cache", str(args.cache), *(["--clang", args.clang] if args.clang else [])] + *(["--newlib-cache", str(args.cache)] if args.cache else []), + *(["--clang", args.clang] if args.clang else [])] EMPTY_ASM = re.compile(r'^\s*(?:tail )?call void asm sideeffect "",.*\n', re.M) @@ -144,6 +145,7 @@ def compile_one(item): ir = out / (obj.replace("/", "-")[:-2] + ".ll") command = [sys.executable, str(DRIVER), "-c", source, args.optimization, "-D", "__clang_major__=0", # インラインアセンブリを使う Clang 向け分岐を避ける + "-D", "BB_GLOBAL_CONST=", # ptr_to_globals を rodata に置かない(U モードでは書込みフォルトになる) "-I", str(ROOT / "foundation/include/pkcl-posix"), *driver_args(args), "-o", str(ir)] for kind, value in flags: if kind == "-I": @@ -174,7 +176,7 @@ def compile_one(item): def link(args, irs, out: Path): sources = [PKCL / name for name in ("errno.c", "fd.c", "fs.c", "crt.c", "newlib/syscalls.c")] - extra = sorted(PKCL.glob("posix/*.c")) + extra = [*sorted(PKCL.glob("posix/*.c")), HERE / "busybox_stubs.c"] command = [sys.executable, str(DRIVER), *map(str, irs), *map(str, sources), *map(str, extra), args.optimization, "-I", str(ROOT / "foundation/include"), "-I", str(ROOT / "sdk/cpt32/include"), "-I", str(ROOT / "foundation/include/pkcl-posix"), "-D", "PKCL_LIBC", diff --git a/tools/pkcl/busybox_stubs.c b/tools/pkcl/busybox_stubs.c new file mode 100644 index 0000000..04f43ae --- /dev/null +++ b/tools/pkcl/busybox_stubs.c @@ -0,0 +1,15 @@ +/* BusyBox 向けに足す定義(doc/spec_pkcl.md §8)。リンクするのは busybox.py だけで、pkcl 本体には入らない。 + * + * Newlib の nano scanf は浮動小数の読み取りを _scanf_float への弱い参照で呼ぶ。弱いシンボルがないので + * 常に strtod 一式(約 60 KB)が入り、BusyBox を 1 MiB(FND_EXEC_IMAGE_MAX)に収められなくなる。 + * BusyBox は scanf で浮動小数を読まないので、一致失敗を返す定義に置き換える。 */ +#include +#include + +struct _reent; +struct _scan_data_t; + +int _scanf_float(struct _reent *data, void *pdata, void *fp, va_list *ap) { + (void)data, (void)pdata, (void)fp, (void)ap; + return -1; +}