From afc34a140fd0380fe28a9db0c4087bf385ffb909 Mon Sep 17 00:00:00 2001 From: Pratik Bhujel Date: Tue, 6 Oct 2026 17:52:31 +0545 Subject: [PATCH 1/2] feat(examples): add cross-platform native terminal I/O example for Linux, macOS, and Windows Demonstrate how to build cross-platform native CLI and TUI applications with TypePHP using mixed C++/PHP programming (cpp-src + php-src). - Native terminal sizing via ioctl(TIOCGWINSZ) on POSIX and GetConsoleScreenBufferInfo on Windows - Native in-process TTY stream check (isatty / GetConsoleMode) - Native raw mode with atexit automated cleanup to prevent terminal corruption - In-process secret password input without terminal echo - Window title setting via OSC escape codes (POSIX) and SetConsoleTitleW (Windows) - ANSI color detection probing TERM and NO_COLOR - Object-oriented Terminal wrapper class with helper methods - Interactive CLI demonstration program - Comprehensive bilingual documentation and PHPUnit test --- examples/terminal/README.md | 103 ++++++++++ examples/terminal/cpp-src/terminal.cc | 271 +++++++++++++++++++++++++ examples/terminal/main.php | 49 +++++ examples/terminal/php-src/Terminal.php | 142 +++++++++++++ examples/terminal/project.yml | 9 + phpunit/TerminalExampleTest.php | 54 +++++ 6 files changed, 628 insertions(+) create mode 100644 examples/terminal/README.md create mode 100644 examples/terminal/cpp-src/terminal.cc create mode 100644 examples/terminal/main.php create mode 100644 examples/terminal/php-src/Terminal.php create mode 100644 examples/terminal/project.yml create mode 100644 phpunit/TerminalExampleTest.php diff --git a/examples/terminal/README.md b/examples/terminal/README.md new file mode 100644 index 00000000..ab948408 --- /dev/null +++ b/examples/terminal/README.md @@ -0,0 +1,103 @@ +# Cross-Platform Native Terminal Demo / 跨平台原生终端示例 + +[English](#english) | [简体中文](#简体中文) + +--- + + +## English + +This example demonstrates how to build a **cross-platform native CLI/TUI application** with TypePHP using mixed C++ and PHP programming (`cpp-src` + `php-src`). + +Standard PHP CLI applications traditionally depend on executing external processes (`stty`, `mode CON`) or installing separate PHP extensions to query window size, enable raw mode, or capture hidden passwords. When compiled into a standalone native binary with TypePHP (`tpc -m bin`), this implementation interacts directly with the operating system kernel and console APIs with zero external dependencies and zero `zval` boxing overhead on hot paths. + +### Supported Operating Systems + +- **Linux** (`x86_64`, `aarch64`): POSIX `termios`, `sys/ioctl.h` +- **macOS** (`Apple Silicon`, `Intel`): Darwin POSIX `termios`, `sys/ioctl.h` +- **Windows** (`x86_64`): Win32 Console API (`GetStdHandle`, `SetConsoleMode`, `ReadConsoleInputW`, `SetConsoleTitleW`) + +### Features Implemented + +1. **Terminal Sizing**: Queries columns and lines via `ioctl(TIOCGWINSZ)` on Unix and `GetConsoleScreenBufferInfo` on Windows. +2. **TTY Detection**: Fast in-process check for interactive terminal stream (`isatty` / `GetConsoleMode`). +3. **Raw Mode**: Non-canonical raw input mode without line buffering, featuring automatic terminal restoration on exit (`atexit`) to prevent terminal corruption. +4. **Secret Password Input**: In-process hidden input without echo, restoring terminal flags immediately upon completion. +5. **Window Title**: Sets terminal/console window title via OSC escape sequence on Unix and `SetConsoleTitleW` (UTF-8 to UTF-16) on Windows. +6. **ANSI Color Support**: Probes environment variables (`TERM`, `NO_COLOR`) to determine formatting compatibility. + +### Project Structure + +``` +examples/terminal/ +├── project.yml # TypePHP build configuration +├── main.php # Interactive CLI demonstration entrypoint +├── cpp-src/ +│ └── terminal.cc # Cross-platform C++ implementation (POSIX & Win32) +└── php-src/ + └── Terminal.php # PHP stubs and object-oriented Terminal wrapper +``` + +### Compilation & Usage + +Compile the project into a standalone executable: + +```bash +php bin/tpc.php examples/terminal/project.yml -o terminal-demo +``` + +Run the compiled executable: + +```bash +./terminal-demo +``` + +--- + + +## 简体中文 + +本示例展示了如何使用 TypePHP 的 C++/PHP 混合编程能力(`cpp-src` + `php-src`),构建**跨平台原生命令行与终端(CLI/TUI)应用程序**。 + +在传统 PHP 命令行开发中,通常需要调用外部进程(例如 `stty` 或 Windows `mode CON`)或依赖额外安装的 C 扩展来获取终端尺寸、开启 Raw 模式或读取无回显密码。借助 TypePHP 编译为独立二进制(`tpc -m bin`)后,本实现直接调用底层操作系统内核与控制台 API,具备零外部依赖、零进程开销以及原生 C++ 调用性能。 + +### 支持的操作系统 + +- **Linux** (`x86_64`, `aarch64`):基于 POSIX `termios`、`sys/ioctl.h` +- **macOS** (`Apple Silicon`, `Intel`):基于 Darwin POSIX `termios`、`sys/ioctl.h` +- **Windows** (`x86_64`):基于 Win32 控制台 API(`GetStdHandle`、`SetConsoleMode`、`ReadConsoleInputW`、`SetConsoleTitleW`) + +### 核心功能 + +1. **终端窗口尺寸**:在 Unix 下调用 `ioctl(TIOCGWINSZ)`,在 Windows 下调用 `GetConsoleScreenBufferInfo`,快速读取行列数。 +2. **TTY 终端检测**:原生物理终端连接检测(`isatty` / `GetConsoleMode`)。 +3. **Raw 模式**:禁用规范输入行缓冲,支持单键即时捕获,并注册 `atexit` 自动安全恢复终端状态,防止终端异常。 +4. **安全密码输入**:进程内无回显密码读取,读取完成后自动重置控制台标志。 +5. **设置窗口标题**:Unix 下通过 OSC 转义序列,Windows 下通过 `SetConsoleTitleW`(UTF-8 转 UTF-16)设置控制台标题。 +6. **ANSI 颜色支持探测**:检测 `TERM` 与 `NO_COLOR` 环境变量。 + +### 项目结构 + +``` +examples/terminal/ +├── project.yml # TypePHP 项目编译配置 +├── main.php # 交互式命令行演示入口 +├── cpp-src/ +│ └── terminal.cc # 跨平台 C++ 实现(POSIX 与 Win32) +└── php-src/ + └── Terminal.php # PHP 原生方法签名与面向对象 Terminal 封装类 +``` + +### 编译与运行 + +使用 TypePHP 编译为原生二进制程序: + +```bash +php bin/tpc.php examples/terminal/project.yml -o terminal-demo +``` + +运行编译后的程序: + +```bash +./terminal-demo +``` diff --git a/examples/terminal/cpp-src/terminal.cc b/examples/terminal/cpp-src/terminal.cc new file mode 100644 index 00000000..bd7c5ffd --- /dev/null +++ b/examples/terminal/cpp-src/terminal.cc @@ -0,0 +1,271 @@ +#include + +#if defined(_WIN32) +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +using namespace php; + +#if defined(_WIN32) +static DWORD orig_console_mode = 0; +static bool raw_mode_active = false; + +static void cleanup_terminal_at_exit() +{ + if (raw_mode_active) { + HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); + if (hStdin != INVALID_HANDLE_VALUE && hStdin != NULL) { + SetConsoleMode(hStdin, orig_console_mode); + } + raw_mode_active = false; + } +} +#else +static struct termios orig_termios; +static bool raw_mode_active = false; + +static void cleanup_terminal_at_exit() +{ + if (raw_mode_active) { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); + raw_mode_active = false; + } +} +#endif + +Int php_terminal_get_width() +{ +#if defined(_WIN32) + HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); + if (hStdout != INVALID_HANDLE_VALUE && hStdout != NULL) { + CONSOLE_SCREEN_BUFFER_INFO csbi; + if (GetConsoleScreenBufferInfo(hStdout, &csbi)) { + return static_cast(csbi.srWindow.Right - csbi.srWindow.Left + 1); + } + } + return 80; +#else + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) { + return static_cast(ws.ws_col); + } + return 80; +#endif +} + +Int php_terminal_get_height() +{ +#if defined(_WIN32) + HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); + if (hStdout != INVALID_HANDLE_VALUE && hStdout != NULL) { + CONSOLE_SCREEN_BUFFER_INFO csbi; + if (GetConsoleScreenBufferInfo(hStdout, &csbi)) { + return static_cast(csbi.srWindow.Bottom - csbi.srWindow.Top + 1); + } + } + return 24; +#else + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_row > 0) { + return static_cast(ws.ws_row); + } + return 24; +#endif +} + +Bool php_terminal_isatty(Int fd) +{ +#if defined(_WIN32) + HANDLE h = (fd == 0) ? GetStdHandle(STD_INPUT_HANDLE) : + ((fd == 1) ? GetStdHandle(STD_OUTPUT_HANDLE) : GetStdHandle(STD_ERROR_HANDLE)); + if (h == INVALID_HANDLE_VALUE || h == NULL) { + return false; + } + DWORD mode; + return GetConsoleMode(h, &mode) != 0; +#else + return isatty(static_cast(fd)) != 0; +#endif +} + +Bool php_terminal_set_raw_mode(Bool enable) +{ +#if defined(_WIN32) + HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); + if (hStdin == INVALID_HANDLE_VALUE || hStdin == NULL) { + return false; + } + + if (enable) { + if (!raw_mode_active) { + if (!GetConsoleMode(hStdin, &orig_console_mode)) { + return false; + } + atexit(cleanup_terminal_at_exit); + } + DWORD raw_mode = orig_console_mode & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT); + if (SetConsoleMode(hStdin, raw_mode)) { + raw_mode_active = true; + return true; + } + return false; + } else { + if (raw_mode_active) { + SetConsoleMode(hStdin, orig_console_mode); + raw_mode_active = false; + } + return true; + } +#else + if (enable) { + if (!raw_mode_active) { + if (tcgetattr(STDIN_FILENO, &orig_termios) == -1) { + return false; + } + atexit(cleanup_terminal_at_exit); + } + struct termios raw = orig_termios; + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + raw.c_oflag &= ~(OPOST); + raw.c_cflag |= (CS8); + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + raw.c_cc[VMIN] = 1; + raw.c_cc[VTIME] = 0; + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == 0) { + raw_mode_active = true; + return true; + } + return false; + } else { + if (raw_mode_active) { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); + raw_mode_active = false; + } + return true; + } +#endif +} + +String php_terminal_read_char() +{ +#if defined(_WIN32) + HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); + if (hStdin == INVALID_HANDLE_VALUE || hStdin == NULL) { + return String(""); + } + INPUT_RECORD rec; + DWORD count; + while (ReadConsoleInputW(hStdin, &rec, 1, &count) && count > 0) { + if (rec.EventType == KEY_EVENT && rec.Event.KeyEvent.bKeyDown) { + WCHAR wc = rec.Event.KeyEvent.uChar.UnicodeChar; + if (wc != 0) { + char utf8[8] = {0}; + int len = WideCharToMultiByte(CP_UTF8, 0, &wc, 1, utf8, sizeof(utf8) - 1, NULL, NULL); + if (len > 0) { + return String(utf8, static_cast(len)); + } + } + } + } + return String(""); +#else + char c = 0; + ssize_t n = read(STDIN_FILENO, &c, 1); + if (n > 0) { + return String(&c, 1); + } + return String(""); +#endif +} + +String php_terminal_read_secret(String prompt) +{ + if (!prompt.empty()) { + fwrite(prompt.data(), 1, prompt.length(), stdout); + fflush(stdout); + } + +#if defined(_WIN32) + HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); + DWORD mode = 0; + bool has_console = (hStdin != INVALID_HANDLE_VALUE && hStdin != NULL && GetConsoleMode(hStdin, &mode)); + if (has_console) { + SetConsoleMode(hStdin, mode & (~ENABLE_ECHO_INPUT)); + } + char buf[2048] = {0}; + size_t len = 0; + if (fgets(buf, sizeof(buf), stdin) != nullptr) { + len = strlen(buf); + while (len > 0 && (buf[len - 1] == '\r' || buf[len - 1] == '\n')) { + buf[--len] = '\0'; + } + } + if (has_console) { + SetConsoleMode(hStdin, mode); + } + printf("\n"); + fflush(stdout); + return String(buf, len); +#else + struct termios oldt, newt; + bool has_tty = (tcgetattr(STDIN_FILENO, &oldt) == 0); + if (has_tty) { + newt = oldt; + newt.c_lflag &= ~(ECHO); + tcsetattr(STDIN_FILENO, TCSANOW, &newt); + } + char buf[2048] = {0}; + size_t len = 0; + if (fgets(buf, sizeof(buf), stdin) != nullptr) { + len = strlen(buf); + while (len > 0 && (buf[len - 1] == '\r' || buf[len - 1] == '\n')) { + buf[--len] = '\0'; + } + } + if (has_tty) { + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); + } + printf("\n"); + fflush(stdout); + return String(buf, len); +#endif +} + +Bool php_terminal_set_title(String title) +{ +#if defined(_WIN32) + int wlen = MultiByteToWideChar(CP_UTF8, 0, title.data(), -1, NULL, 0); + if (wlen <= 0) { + return false; + } + auto *wstr = new WCHAR[wlen]; + MultiByteToWideChar(CP_UTF8, 0, title.data(), -1, wstr, wlen); + BOOL ok = SetConsoleTitleW(wstr); + delete[] wstr; + return ok != 0; +#else + printf("\033]0;%s\007", title.data()); + fflush(stdout); + return true; +#endif +} + +Bool php_terminal_supports_color() +{ + const char *term = std::getenv("TERM"); + if (term != nullptr && std::strcmp(term, "dumb") == 0) { + return false; + } + const char *no_color = std::getenv("NO_COLOR"); + if (no_color != nullptr && no_color[0] != '\0') { + return false; + } + return true; +} diff --git a/examples/terminal/main.php b/examples/terminal/main.php new file mode 100644 index 00000000..7091cc3a --- /dev/null +++ b/examples/terminal/main.php @@ -0,0 +1,49 @@ +' : (($key === "\t") ? '' : (($key === "\033") ? '' : $key)); + echo "\n Received key: " . Terminal::colorize($keyDisplay, Terminal::FG_MAGENTA . Terminal::COLOR_BOLD) . " (code: " . ord($key) . ")\n"; + } else { + echo "\n Raw mode not available in current environment.\n"; + } + + echo "\n" . Terminal::colorize('✔ Native terminal operations completed successfully.', Terminal::FG_GREEN . Terminal::COLOR_BOLD) . "\n\n"; +} diff --git a/examples/terminal/php-src/Terminal.php b/examples/terminal/php-src/Terminal.php new file mode 100644 index 00000000..e875dad6 --- /dev/null +++ b/examples/terminal/php-src/Terminal.php @@ -0,0 +1,142 @@ + terminal_get_width(), + 'height' => terminal_get_height(), + ]; + } + + public static function isInteractive(): bool + { + return terminal_isatty(self::STDOUT); + } + + public static function enableRawMode(): bool + { + return terminal_set_raw_mode(true); + } + + public static function disableRawMode(): bool + { + return terminal_set_raw_mode(false); + } + + public static function readChar(): string + { + return terminal_read_char(); + } + + public static function readSecret(string $prompt = ''): string + { + return terminal_read_secret($prompt); + } + + public static function setTitle(string $title): bool + { + return terminal_set_title($title); + } + + public static function supportsColor(): bool + { + return terminal_supports_color(); + } + + public static function clearScreen(): void + { + echo "\033[2J\033[H"; + } + + public static function moveCursor(int $line, int $col): void + { + echo "\033[{$line};{$col}H"; + } + + public static function colorize(string $text, string $colorCode): string + { + if (!self::supportsColor()) { + return $text; + } + return $colorCode . $text . self::COLOR_RESET; + } +} diff --git a/examples/terminal/project.yml b/examples/terminal/project.yml new file mode 100644 index 00000000..cf22f473 --- /dev/null +++ b/examples/terminal/project.yml @@ -0,0 +1,9 @@ +name: terminal-demo +version: 1.0.0 +mode: bin +cxx-std: c++17 + +sources: + - php-src + - ./cpp-src + - main.php diff --git a/phpunit/TerminalExampleTest.php b/phpunit/TerminalExampleTest.php new file mode 100644 index 00000000..96a59a0e --- /dev/null +++ b/phpunit/TerminalExampleTest.php @@ -0,0 +1,54 @@ + Date: Tue, 6 Oct 2026 18:26:47 +0545 Subject: [PATCH 2/2] fix(compiler): wrap dynamic default expressions in typed helpers and preserve native property compound type checks - DefaultArgumentGenerator: convert dynamic binary expressions emitted as php::Var(...) to native types using php::toInt() / php::toFloat() in typed default argument helper functions. - AssignOpTrait: preserve wrapObjectPropertyAssignTypeCheck on native scalar object properties when compound assignment RHS is a dynamic Variant (Type::VAR). --- src/Generator/DefaultArgumentGenerator.php | 2 +- src/Parser/AssignOpTrait.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Generator/DefaultArgumentGenerator.php b/src/Generator/DefaultArgumentGenerator.php index 357ac581..97e628a0 100644 --- a/src/Generator/DefaultArgumentGenerator.php +++ b/src/Generator/DefaultArgumentGenerator.php @@ -108,7 +108,7 @@ protected function genDefaultArgumentHelperDefinitions(): string */ private function convertRuntimeConstantDefault(string $type, string $default): string { - if (!str_contains($default, 'php::constant(')) { + if (!str_contains($default, 'php::constant(') && !str_contains($default, 'php::Var(')) { return $default; } diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 328eae90..29eac1ea 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -1345,6 +1345,7 @@ protected function parseNativePropertyAssignOp(Expr\AssignOp $node, string $op): } if (!$this->isAssignOpConcat($op) && $this->hasDynamicScalarOperand($def->type, $rightType) + && $rightType !== Type::VAR && !in_array($rightType, [Type::BIGINT, Type::DECIMAL, Type::BIGFLOAT], true) ) { if (!$this->isNativeObjectClass($this->detectClassOfExpr($node->var->var))) {