Port wxc from C++ to Rust - #29
Conversation
49e12d0 to
8090c73
Compare
Rewrite the entire wxc (Windows Container Executor) codebase from C++ to
Rust, replacing the Visual Studio / vcxproj build system with a Cargo
workspace. Update documentation, build scripts, and SDK/CLI tooling to
reflect the new Rust-based build.
- **wxc**: CLI entry point (wxc-exec binary) using clap for argument
parsing. Supports config via file path, --config flag, or
--config-base64. Includes --delete mode for container profile cleanup.
- **wxc_common**: Shared library containing all core modules:
- appcontainer: AppContainer profile creation and sandboxed script
execution via Win32 Security Isolation APIs
- config_parser: JSON configuration loading from file or base64
- models: Data models (CodexRequest, ScriptResponse, NetworkPolicy,
FilesystemPolicy, etc.) with serde serialization
- error: Unified WxcError enum using thiserror
- filesystem_bfs: BFS-based filesystem permission manager for
read-write and read-only path policies
- network_firewall: Windows Firewall rule management via COM/INetFwPolicy2
for network allow/block enforcement
- process_util: Process creation, pipe I/O, and output capture utilities
- script_runner: Script execution orchestration
- string_util: Wide-string (PCWSTR/HSTRING) conversion helpers
- logger: Logging with debug and event-log modes
- validator: Script request validation
- **wxc_test_driver**: Test harness binary (wxc-test-driver) for
integration testing
Remove all original C++ implementation files, headers, PCH files, and
Visual Studio project/filter files for wxc, wxc_common, wxc_test_driver,
and wxc_tests. Remove leftover C++ build artifacts: wxc.sln,
vcpkg.json, .clang-format, Directory.Build.props, build_all.bat,
build_debug.bat, and build_tests.bat.
Replace the C++ build steps (MSBuild, vcpkg) in .github/workflows/build.yml
with Rust toolchain setup (rustup), cargo build, and cargo test steps.
Use the pre-installed rustup on GitHub-hosted runners.
- Readme.md: Replace VS 2022/C++/vcpkg prerequisites with Rust toolchain
and cargo build instructions
- build.bat: Replace msbuild wxc.sln with cargo build --release
- sdk/README.md: Update build instructions to reference cargo
- cli/ARCHITECTURE.md: Update C++ reference to Rust
- cli/src/cli.ts: Update default wxc-exec.exe path from x64/Debug to
src/target/debug
- cli/example.ts: Same path update
- sdk/src/platform.ts: Update findWxcExecutable() to search Rust build
output paths (src/target/{release,debug}) instead of VS layout
(bin/{x64,ARM64})
- .gitignore: Add **/target/ for Rust build artifacts
- examples/08_pwsh.json: New PowerShell AppContainer example config
- src/expert-code-review-results.md: Code review findings from the port
Key dependencies: windows 0.58 crate (Win32 API bindings), serde/serde_json,
clap 4, thiserror, anyhow, base64.
Co-authored-by: shschaefer <stuart@theschaefers.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
8090c73 to
cf4a5b0
Compare
| path.join(__dirname, '..', 'bin', platformDir, 'wxc-exec.exe'), | ||
| // Relative to project when used as a node module | ||
| path.join(__dirname, '..', 'node_modules', '@shschaefer', 'wxc-sdk', 'bin', platformDir, 'wxc-exec.exe'), | ||
| // Rust release build output |
There was a problem hiding this comment.
We need to adjust this. It is not quite the same as what was there before. Also, how are you handling platform builds with Rust?
There was a problem hiding this comment.
For differing x64 and arm64 windows builds in Rust we'd need to do something like this (for debug no --release):
cargo build --release --target x86_64-pc-windows-msvc
cargo build --release --target aarch64-pc-windows-msvcwhich produces:
target/x86_64-pc-windows-msvc/release/wxc.exe
target/aarch64-pc-windows-msvc/release/wxc.exeSo the paths in this PR would need to change. Note: VS does not support rust only VS Code. The equivalent of the cpp post build event here in VS would be to create a tasks.json file in the .vscode/ folder. In there we would use powershell to run the cargo commands and copy the exes to where ever we wanted.
Going a bit further though, as we think about distribution. I think we should leverage npm's optional dependency feature as it will automatically install the dependency package based on the hosts OS and CPU.
We can do something like this:
- publish
wxc-exec-win-x64to npm: (prebuilt wxc-exec.exe)- Package.json :
{ "name": "@microsoft/wxc-exec-win-x64", "os": ["win32"], "cpu": ["x64"], "files": ["wxc-exec.exe"] } - publish
wxc-exec-win-arm64to npm: (prebuilt wxc-exec.exe)- Package.json :
{ "name": "@microsoft/wxc-exec-win-arm64", "os": ["win32"], "cpu": ["arm64"], "files": ["wxc-exec.exe"] } - publish
mxc-sdkto npm- Package.json :
{ "name": "@microsoft/wxc-sdk", "optionalDependencies": { "@microsoft/wxc-win-x64": "0.1.x", "@microsoft/wxc-win-arm64": "0.1.x" } }
Then the SDK will just need to do something like this to get the path to the executable:
import os from "os"
export function getWxcBinaryPath(): string {
const platform = os.platform();
const arch = os.arch();
if (platform !== 'win32') {
throw new Error(`Unsupported OS: ${platform}. WXC currently supports Windows only.`);
}
let packageName: string;
if (arch === 'x64') {
packageName = '@microsoft/wxc-exec-win-x64';
} else if (arch === 'arm64') {
packageName = '@microsoft/wxc-exec-win-arm64';
} else {
throw new Error(`Unsupported architecture: ${arch}`);
}
try {
return require.resolve(`${packageName}/wxc-exec.exe`);
} catch {
throw new Error(
`Failed to find wxc-exec binary. The platform package "${packageName}" may not be installed. `
);
}
}There was a problem hiding this comment.
I've updated build.bat to build arm64/x64 release/debug. I'll tackle the package approach in a subsequent PR
| @@ -1 +1 @@ | |||
| name: Build | |||
There was a problem hiding this comment.
We should probably be building the Rust components for both x64 and ARM64 windows targets.
Also, most Rust repositories include two standard checks in their CI pipelines:
rustfmt: enforces consistent code formatting
clippy : a linter that catches common mistakes and non-idiomatic patterns
These tools are widely used across the Rust ecosystem and are typically enforced in CI to ensure code quality and consistency. We might as well start now while we're early.
I also suggest splitting the SDK build from the wxc build. The SDK is currently Node/TypeScript, while wxc is a Rust binary crate, and the SDK could evolve to other languages in the future. Separating them will keep the pipeline easier to maintain and reason about.
I think this file should look something this, which should cover formatting, linting, and tests across the supported architectures for rust:
name: Build
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
# Check formatting and linting for the WXC Rust code
wxc-exec-lint:
name: WXC Exec Lint
runs-on: windows-latest
defaults:
run:
working-directory: src
steps:
- uses: actions/checkout@v4
- name: Setup Rust toolchain
run: |
rustup update stable
rustup component add rustfmt clippy
- name: Check formatting
id: fmt
continue-on-error: true
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Fail if formatting check failed
if: steps.fmt.outcome == 'failure'
run: exit 1
# Build and run Rust tests on x64 and ARM64
wxc-exec-test:
name: WXC Exec Rust tests
strategy:
matrix:
include:
- target: x86_64-pc-windows-msvc
runner: windows-2025
- target: aarch64-pc-windows-msvc
runner: windows-11-arm
runs-on: ${{ matrix.runner }}
defaults:
run:
working-directory: src
steps:
- uses: actions/checkout@v4
- name: Setup Rust toolchain
run: |
rustup update stable
rustup target add ${{ matrix.target }}
- name: Run tests
run: cargo test --workspace
# Build and package the TypeScript SDK
wxc-typescript-sdk:
name: WXC TypeScript SDK
runs-on: windows-latest
defaults:
run:
working-directory: sdk
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Pack npm package
run: npm pack
- uses: actions/upload-artifact@v4
with:
name: sdk-npm-package
path: sdk/*.tgzIf you take my change above. You can run the same checks locally before pushing:
Format Rust code automatically: cargo fmt --all
Check formatting (same as CI): cargo fmt --all -- --check
Run the linter: cargo clippy --workspace --all-targets -- -D warnings
Run tests: cargo test --workspace
If needed, install them via cmdline: rustup component add rustfmt clippy
I found that installing the GitHub CLI enables GitHub Copilot to retrieve build pipeline failure information and quickly fix, commit, and push changes if you tell it to. Note: GitHub CLI is different from the GitHub Copilot CLI.
There was a problem hiding this comment.
Agree, we should add clippy and rustfmt (only if we add a custom format, some of the standard rules are rough).
| @@ -0,0 +1,139 @@ | |||
| use std::fmt::Write; | |||
There was a problem hiding this comment.
FYI all rust files should have the following copyright header:
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.See: https://docs.opensource.microsoft.com/releasing/general-guidance/copyright-headers/
There was a problem hiding this comment.
Can we do this as a commit/PR, rather than comment?
| fn log_request(request: &CodexRequest, logger: &mut Logger) { | ||
| let _ = writeln!(logger, "Script code length: {}", request.script_code.len()); | ||
| let _ = writeln!(logger, "Working directory: {}", request.working_directory); | ||
| let _ = writeln!(logger, "Script timeout: {}", request.script_timeout); | ||
| let _ = writeln!( | ||
| logger, | ||
| "Container name: {}", | ||
| request.policy.app_container_name | ||
| ); | ||
| } | ||
|
|
||
| fn display_script_results(response: &ScriptResponse, logger: &mut Logger) { | ||
| let _ = writeln!(logger, "Exit code: {}", response.exit_code); |
There was a problem hiding this comment.
I assume it's written like this because we don't care about logging failures? On the off chance we did care about them. We can return a fmt::Result and use the ? operator which acts like our short circuit RETURN_IF_FAILED macros in c++, so something like:
fn log_request(request: &CodexRequest, logger: &mut Logger) -> fmt::Result {
writeln!(logger, "Script code length: {}", request.script_code.len())?;
// other writeln statements below
}|
|
||
| /// Remove a specific Python "Failed to find real location of ..." line from stderr. | ||
| pub fn suppress_python_location_error(stderr: &mut String) { | ||
| let needle = "Failed to find real location of "; |
There was a problem hiding this comment.
Hmm, I'm not sure if this error is localized based on the system or if it's always in english.
| path.join(__dirname, '..', 'bin', platformDir, 'wxc-exec.exe'), | ||
| // Relative to project when used as a node module | ||
| path.join(__dirname, '..', 'node_modules', '@shschaefer', 'wxc-sdk', 'bin', platformDir, 'wxc-exec.exe'), | ||
| // Rust release build output |
There was a problem hiding this comment.
For differing x64 and arm64 windows builds in Rust we'd need to do something like this (for debug no --release):
cargo build --release --target x86_64-pc-windows-msvc
cargo build --release --target aarch64-pc-windows-msvcwhich produces:
target/x86_64-pc-windows-msvc/release/wxc.exe
target/aarch64-pc-windows-msvc/release/wxc.exeSo the paths in this PR would need to change. Note: VS does not support rust only VS Code. The equivalent of the cpp post build event here in VS would be to create a tasks.json file in the .vscode/ folder. In there we would use powershell to run the cargo commands and copy the exes to where ever we wanted.
Going a bit further though, as we think about distribution. I think we should leverage npm's optional dependency feature as it will automatically install the dependency package based on the hosts OS and CPU.
We can do something like this:
- publish
wxc-exec-win-x64to npm: (prebuilt wxc-exec.exe)- Package.json :
{ "name": "@microsoft/wxc-exec-win-x64", "os": ["win32"], "cpu": ["x64"], "files": ["wxc-exec.exe"] } - publish
wxc-exec-win-arm64to npm: (prebuilt wxc-exec.exe)- Package.json :
{ "name": "@microsoft/wxc-exec-win-arm64", "os": ["win32"], "cpu": ["arm64"], "files": ["wxc-exec.exe"] } - publish
mxc-sdkto npm- Package.json :
{ "name": "@microsoft/wxc-sdk", "optionalDependencies": { "@microsoft/wxc-win-x64": "0.1.x", "@microsoft/wxc-win-arm64": "0.1.x" } }
Then the SDK will just need to do something like this to get the path to the executable:
import os from "os"
export function getWxcBinaryPath(): string {
const platform = os.platform();
const arch = os.arch();
if (platform !== 'win32') {
throw new Error(`Unsupported OS: ${platform}. WXC currently supports Windows only.`);
}
let packageName: string;
if (arch === 'x64') {
packageName = '@microsoft/wxc-exec-win-x64';
} else if (arch === 'arm64') {
packageName = '@microsoft/wxc-exec-win-arm64';
} else {
throw new Error(`Unsupported architecture: ${arch}`);
}
try {
return require.resolve(`${packageName}/wxc-exec.exe`);
} catch {
throw new Error(
`Failed to find wxc-exec binary. The platform package "${packageName}" may not be installed. `
);
}
}| thiserror = "2" | ||
| anyhow = "1" | ||
| base64 = "0.22" | ||
| clap = { version = "4", features = ["derive"] } |
There was a problem hiding this comment.
we should add wxc_common = { path = "wxc_common" } here as well and probably sort this list
| /** | ||
| * Check Windows build version requirements for WXC | ||
| * | ||
| * Requirements: | ||
| * - Registry key HKLM\Software\Microsoft\Windows NT\CurrentVersion\BuildLab must exist | ||
| * - BuildLab format: buildNumber.branch.buildDate | ||
| * - Branch must be "ge_current_directwinai*" | ||
| * - Build number must be >= 26559 | ||
| * | ||
| * @returns true if Windows build meets requirements, false otherwise | ||
| */ | ||
| function checkWindowsBuildVersion(): boolean { |
There was a problem hiding this comment.
I think we'll need to update this before release
There was a problem hiding this comment.
I have asked Anis to get me updated information. Will take care of this in a separate PR.
Branden Bonaby (bbonaby)
left a comment
There was a problem hiding this comment.
Spoke to Stuart, we'll check this in and I'll do my comments as a PR. Thanks Martin
Rewrite the entire wxc (Windows Container Executor) codebase from C++ to
Rust, replacing the Visual Studio / vcxproj build system with a Cargo
workspace. Update documentation, build scripts, and SDK/CLI tooling to
reflect the new Rust-based build.
New Rust crate structure (src/)
parsing. Supports config via file path, --config flag, or
--config-base64. Includes --delete mode for container profile cleanup.
execution via Win32 Security Isolation APIs
FilesystemPolicy, etc.) with serde serialization
read-write and read-only path policies
for network allow/block enforcement
integration testing
Deleted C++ sources
Remove all original C++ implementation files, headers, PCH files, and
Visual Studio project/filter files for wxc, wxc_common, wxc_test_driver,
and wxc_tests. Remove leftover C++ build artifacts: wxc.sln,
vcpkg.json, .clang-format, Directory.Build.props, build_all.bat,
build_debug.bat, and build_tests.bat.
Updated CI pipeline for Rust
Replace the C++ build steps (MSBuild, vcpkg) in .github/workflows/build.yml
with Rust toolchain setup (rustup), cargo build, and cargo test steps.
Use the pre-installed rustup on GitHub-hosted runners.
Updated docs and tooling for Rust migration
and cargo build instructions
src/target/debug
output paths (src/target/{release,debug}) instead of VS layout
(bin/{x64,ARM64})
Other changes
Key dependencies: windows 0.58 crate (Win32 API bindings), serde/serde_json,
clap 4, thiserror, anyhow, base64.
Co-authored-by: shschaefer stuart@theschaefers.com
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com