Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/actions/mtest/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: mtest
description: >
The mtest engine the conformance suite reports to: the release the workflow pins (MTEST_VERSION), an
xlings package (xim:mtest) that the index verifies, else the release bundle checked against MTEST_SHA256;
MTEST names its launcher. A job that has no xlings yet gets the pinned one first (XLINGS_VERSION).
runs:
using: composite
steps:
- name: xlings, where the job has none
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
command -v xlings > /dev/null 2>&1 && exit 0
curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 \
https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \
| bash -s "$XLINGS_VERSION"
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
- name: xlings, where the job has none (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
if (Get-Command xlings -ErrorAction SilentlyContinue) { exit 0 }
irm https://d2learn.org/xlings-install.ps1.txt | iex
"$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# xlings by the configured mirror (GLOBAL), then by CN (GitCode first), each with a refreshed index
# (a cached index can predate the release, and xlings keeps it when an anonymous refresh fails); else
# the release bundle itself, checked against MTEST_SHA256: the web address, then the API's, which
# answered 200 while the web one answered 504 (GitHub's 2026-11-09 incident).
launcher=""
for mirror in GLOBAL CN; do
xlings config --mirror "$mirror" > /dev/null 2>&1 || true
xlings update > /dev/null 2>&1 || true
if xlings install "mtest@$MTEST_VERSION" -y -g; then launcher=$(command -v mtest); break; fi
done
# The package's own launcher, not the xvm shim: on Windows the shim runs the package's alias through
# cmd, which reads `&` in an argument (`--tag "on:win32-x64&release"`) as a second command.
exe=''; [ "$RUNNER_OS" != Windows ] || exe='.exe'
own="${XLINGS_HOME:-$HOME/.xlings}/data/xpkgs/xim-x-mtest/$MTEST_VERSION/bin/mtest$exe"
if [ -n "$launcher" ] && [ -f "$own" ]; then launcher="$own"; fi
xlings config --mirror GLOBAL > /dev/null 2>&1 || true
if [ -z "$launcher" ]; then
echo "::warning::xlings could not install mtest $MTEST_VERSION; taking the release itself"
archive="mtest-$MTEST_VERSION-openkal-bundle-all.tar.gz"
temp="$RUNNER_TEMP"; python=python3
if [ "$RUNNER_OS" = Windows ]; then temp=$(cygpath -u "$RUNNER_TEMP"); python=python; fi
if ! curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -o "$temp/$archive" \
"https://github.com/mcpp-community/mtest/releases/download/v$MTEST_VERSION/$archive"; then
id=$(curl -fsSL --retry 3 --retry-all-errors -H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/repos/mcpp-community/mtest/releases/tags/v$MTEST_VERSION" \
| "$python" -c 'import json,sys; print(next(str(a["id"]) for a in json.load(sys.stdin)["assets"] if a["name"] == sys.argv[1]))' "$archive")
curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/octet-stream" -o "$temp/$archive" \
"https://api.github.com/repos/mcpp-community/mtest/releases/assets/$id"
fi
actual=$("$python" -c "import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$temp/$archive")
[ "$actual" = "$MTEST_SHA256" ] || { echo "::error::$archive: sha256 $actual, the workflow pins $MTEST_SHA256"; exit 1; }
mkdir -p "$temp/mtest" && tar xzf "$temp/$archive" -C "$temp/mtest"
launcher="$temp/mtest/mtest-$MTEST_VERSION-openkal-bundle-all/bin/mtest"
[ "$RUNNER_OS" = Windows ] && launcher="$launcher.exe"
fi
"$launcher" --version
"$launcher" --version | grep -qF "$MTEST_VERSION" || { echo "::error::the mtest at $launcher is not $MTEST_VERSION"; exit 1; }
echo "MTEST=$launcher" >> "$GITHUB_ENV"
77 changes: 76 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ on:
default: ""
env:
MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }}
MCPP_VERSION: 2026.11.5.1
MCPP_VERSION: 2026.11.9.1
XLINGS_VERSION: v2026.10.26.1
# The mtest engine the runs are reported with: the conformance suite's cases
# (tools/run-conformance.sh with MTEST set) and `mcpp test`'s store (target/mtest).
MTEST_VERSION: 0.5.2
MTEST_SHA256: be443f8c546b3f4628dcd3f1eb6f45b4d698a7f805232f966349faf8250863c6
XLINGS_NON_INTERACTIVE: '1'

jobs:
Expand Down Expand Up @@ -245,6 +249,8 @@ jobs:
esac
mcpp toolchain list

- uses: ./.github/actions/mtest

- name: Every interface, every kind of examination
run: |
extra=''
Expand Down Expand Up @@ -289,6 +295,9 @@ jobs:
run: |
extra=''
[ -n '${{ matrix.target }}' ] && extra='--target ${{ matrix.target }}'
# The gcc/gnu-CRT leg tests under `gnu-odr' (mcpp.toml says why): the
# dev profile, with the link option a GCC defect on PE needs.
case '${{ matrix.toolchain }}' in gcc@*) extra="$extra --profile gnu-odr" ;; esac
mcpp test $extra 2>&1 | tee tests.log
missing=0
for f in tests/*.cpp; do
Expand All @@ -298,6 +307,11 @@ jobs:
test "$missing" -eq 0
git checkout -- mcpp.toml

- name: This package's test runs, kept for the report
if: always()
run: |
if [ -d target/mtest ]; then cp -R target/mtest "$RUNNER_TEMP/mcpp-test-runs"; fi

# ---------------------------------------------------------------------------
# From a system that is not this one.
#
Expand All @@ -307,6 +321,39 @@ jobs:
# that read this environment's object format and are not on it, and a package
# that only builds on the system it targets is a package a contributor on any
# other system cannot work on.

# A FAILED RUN, EXPLAINED: through the engine the suite's own text is not in
# the log, so the failing cases are named here with what each observed.
- name: The failing cases, explained
if: failure()
run: |
[ -n "${MTEST:-}" ] && [ -d .spec/conformance/.mtest ] || exit 0
( cd .spec/conformance && "$MTEST" explain ) || true

# THE RUNS OF THIS JOB, REPORTED: the conformance suite's (one case per
# interface, its observations within it) and `mcpp test`'s (one case per
# test program), taken into one store, written as the job's summary and kept
# for the report over every job.
- name: The runs, reported
if: always()
run: |
[ -n "${MTEST:-}" ] || { echo "no mtest engine in this job"; exit 0; }
store="$(cygpath -u "$RUNNER_TEMP" 2> /dev/null || printf '%s' "$RUNNER_TEMP")/mtest-store"
mkdir -p "$store"; here="$PWD"
for from in "$here/.spec/conformance/.mtest" "$RUNNER_TEMP/mcpp-test-runs"; do
if [ -f "$from/index.ndjson" ]; then ( cd "$store" && "$MTEST" store import --from "$from" ); fi
done
[ -f "$store/.mtest/index.ndjson" ] || { echo "no run was kept"; exit 0; }
( cd "$store" && "$MTEST" report --format md --group "$GITHUB_RUN_ID" ) >> "$GITHUB_STEP_SUMMARY"

- uses: actions/upload-artifact@v4
if: always()
with:
name: mtest-${{ github.job }}-${{ strategy.job-index }}
path: ${{ runner.temp }}/mtest-store/.mtest
include-hidden-files: true
if-no-files-found: ignore
retention-days: 7
cross:
name: cross build, run under wine, and examine the objects
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -534,3 +581,31 @@ jobs:
fi
rm -f src/probe.cpp
echo "a dependence upon a C runtime is visible to the check"

# ---------------------------------------------------------------------------
# EVERY JOB'S RUNS AS ONE REPORT (`mtest ci report`, the workflow run's group).
report:
name: tests (report)
needs: conformance
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/mtest
- uses: actions/download-artifact@v4
with:
pattern: mtest-*
path: ${{ runner.temp }}/stores
- name: Every job's runs, reported together
shell: bash
run: |
set -euo pipefail
found=0
for store in "$RUNNER_TEMP"/stores/*/; do
[ -f "$store/index.ndjson" ] || continue
"$MTEST" store import --from "$store"
found=1
done
if [ "$found" = 0 ]; then echo "no job kept a run" >> "$GITHUB_STEP_SUMMARY"; exit 0; fi
"$MTEST" ci report --out "$RUNNER_TEMP/tests.md"
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,31 @@ An implementation of [openkal](https://github.com/mcpplibs/openkal) for Windows.

```toml
[dependencies]
openkal = "0.24.0"
openkal = "0.25.0"

[target.'cfg(windows)'.dependencies]
openkal-windows = "0.20.0"
openkal-windows = "0.21.0"
```

Its purpose is as much to test the specification as to be used. openkal was
drawn against systems that share an ancestry; a third implementation on a system
that shares none of it is what turns "this interface is not Unix's shape" from a
claim into an observation.

## What 0.21 adds (openkal 0.25)

**A second name for a file** (`kal_fs_hard_link`) is the link record of
`NtSetInformationFile`, which has the rename record's layout; the file is opened by
its name without following a final link, as `CreateHardLinkW` opens it.
`KAL_FS_PROP_HARD_LINKS` is claimed where the volume reports
`FILE_SUPPORTS_HARD_LINKS`. A directory is refused with `kal_err_is_directory`
before the call, and a name on another volume (`STATUS_NOT_SAME_DEVICE`) with
`kal_err_not_supported`.

**The requests to end are counted.** The console control routine adds one to the
stop word for each event, atomically because events may overlap, and skips zero
when the count wraps.

## Observable process units

The process interface provides the OpenKal 0.24 declared-unit operations.
Expand Down
32 changes: 29 additions & 3 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "openkal-windows"
version = "0.20.0"
version = "0.21.0"
description = "An implementation of openkal for Windows, written on the Win32 interfaces and the object manager beneath them, using no C runtime symbol."
license = "Apache-2.0"

Expand Down Expand Up @@ -66,7 +66,7 @@ provides-interfaces = [
]

[dependencies]
openkal = "0.24.0"
openkal = "0.25.0"

# The package contributes definitions and no modules. The interface it
# implements is declared by the specification package, which this package
Expand Down Expand Up @@ -125,7 +125,14 @@ ldflags = ["-lntdll", "-lsynchronization", "-lshell32", "-lkernel32", "-lbcrypt"
# built with `--release` came out shortened by a different amount. The dev
# profile does not optimize and never showed it, which is also why the check
# below now builds both profiles.
cxxflags = ["-fno-exceptions", "-fno-rtti", "-fno-builtin"]
#
# The flags are attached to this package's own sources. The tests are
# consumers: they import the test library, which imports `std`, and the
# prebuilt `std` interface is compiled with exceptions and run-time type
# information, which a unit compiled without them cannot import.
flags = [
{ glob = "src/**", cxxflags = ["-fno-exceptions", "-fno-rtti", "-fno-builtin"] },
]

# The other ABI names them in the sources instead --- src/win.cpp for the four
# above and src/random.cpp for bcrypt --- where its compilers record
Expand All @@ -146,3 +153,22 @@ default = []
# the loader, and this implementation adds nothing. Where it does not, the entry
# point is this implementation's, and the link line names it.
standalone = { defines = ["OKW_STANDALONE"] }

# The tests are written with the mtest library (mcpp-community/mtest): each file
# declares its cases, and a test that starts a copy of itself declares what the
# copy does as a `child'.
[dev-dependencies]
mcpplibs.mtest = "0.5.2"

# THE TESTS ON THE GNU ABI WITH GCC, which CI runs under this profile. GCC on PE
# emits the function-local static `std::_Sp_make_shared_tag::_S_ti()::__tag' of
# an imported std as a definition that is not COMDAT, once in every object that
# imports std --- the test library's and mcpplibs.os's --- and once in
# libstdc++.a, so the link of every test is refused as a multiple definition.
# The definitions are one entity, and this profile's link accepts the repeats.
# A profile is the root's own, so the option never reaches a consumer's link.
# Otherwise it is the dev profile, so the leg tests what dev tests.
[profile.gnu-odr]
opt = 0
debug = true
ldflags = ["-Wl,--allow-multiple-definition"]
60 changes: 60 additions & 0 deletions src/fs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,62 @@ int kal_fs_rename(kal_dir from, const char* a, kal_uintptr alen,
return rename_with(from, a, alen, to, b, blen, true);
}

// A SECOND NAME FOR A FILE (0.25) is the link record, which has the rename
// record's layout: the file is opened by its name without following a final
// link, as `CreateHardLinkW' opens it, and given `b' beside `to'. A directory is
// answered before the call, because the object manager's refusal names a
// different condition; a name on another volume is STATUS_NOT_SAME_DEVICE,
// which this interface states as an operation the pair cannot perform.
int kal_fs_hard_link(kal_dir from, const char* a, kal_uintptr alen,
kal_dir to, const char* b, kal_uintptr blen) {
void* source_root = dir_handle(from);
void* target_root = dir_handle(to);
if (!source_root || !target_root) return kal_err_invalid;
if (!okw::acceptable(a, alen) || !okw::acceptable(b, blen)) return kal_err_invalid;
if ((kal_fs_props(to) & KAL_FS_PROP_HARD_LINKS) == 0) return kal_err_not_supported;
if (refused_write(target_root)) return kal_err_permission;

void* h = nullptr;
const long r = open_relative(source_root, a, alen, FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE,
okw::file_open, okw::file_open_reparse_point | okw::file_open_for_backup_intent, &h);
if (!okw::ok(r)) return okw::translate_nt(r);
{
okw::file_basic_information basic{};
okw::io_status_block s{};
if (okw::ok(okw::NtQueryInformationFile(h, &s, &basic, sizeof basic, okw::file_basic_information_class))
&& (basic.attributes & FILE_ATTRIBUTE_DIRECTORY) != 0
&& (basic.attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) {
okw::NtClose(h);
return kal_err_is_directory;
}
}

okw::wide_name w(b, blen);
if (!w.ok) { okw::NtClose(h); return kal_err_invalid; }
const kal_uintptr storage_bytes = sizeof(okw::file_rename_information)
+ okw::kMaxName * sizeof(wchar_t);
auto* info = static_cast<okw::file_rename_information*>(
kal_alloc(storage_bytes, alignof(okw::file_rename_information)));
if (!info) { okw::NtClose(h); return kal_err_no_memory; }
info->replace_if_exists = 0;
info->root_directory = target_root;
info->file_name_length = w.string.length;
for (unsigned i = 0; i < w.string.length / 2; ++i) info->file_name[i] = w.buffer[i];

okw::io_status_block s{};
const long n = okw::NtSetInformationFile(
h, &s, info, static_cast<unsigned long>(sizeof(okw::file_rename_information)
+ w.string.length),
okw::file_link_information_class);
okw::NtClose(h);
kal_free(info, storage_bytes, alignof(okw::file_rename_information));
if (n == static_cast<long>(0xC0000035) /* STATUS_OBJECT_NAME_COLLISION */) return kal_err_exists;
if (n == static_cast<long>(0xC00000D4) /* STATUS_NOT_SAME_DEVICE */) return kal_err_not_supported;
if (!okw::ok(n)) return okw::translate_nt(n);
wrote(target_root, b, blen);
return kal_ok;
}

int kal_fs_rename_flags(kal_dir from, const char* a, kal_uintptr alen,
kal_dir to, const char* b, kal_uintptr blen, kal_uintptr flags) {
if ((flags & ~static_cast<kal_uintptr>(KAL_RENAME_NO_REPLACE)) != 0) return kal_err_invalid;
Expand Down Expand Up @@ -1545,6 +1601,10 @@ kal_uintptr kal_fs_props(kal_dir d) {
// own control code.
if (a.info.attributes & okw::fs_supports_reparse_points)
p |= KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS;
// A second name for a file (0.25) where the volume reports that it stores
// several names for one file.
if (a.info.attributes & okw::fs_supports_hard_links)
p |= KAL_FS_PROP_HARD_LINKS;
// A rename that does not replace is a field of the rename record (0.17).
p |= KAL_FS_PROP_RENAME_NO_REPLACE;
// A name held open is removed at once (0.17, clause 7.14) where the volume
Expand Down
7 changes: 6 additions & 1 deletion src/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,12 @@ int g_stop_armed = 0;
// installing a routine does not clear, so a request its starter declined
// stays declined.
BOOL OKW_API stop_routine(DWORD) {
g_stop_word = 1;
// EACH REQUEST IS COUNTED (openkal 0.25), and the count skips zero when it
// wraps, so the word is never zero again once a request has arrived. The
// routine runs on a context of the system's for each event, and events may
// overlap, so the addition is atomic.
volatile long* word = reinterpret_cast<volatile long*>(&g_stop_word);
if (okw::atomic::increment(word) == 0) (void)okw::atomic::increment(word);
WakeByAddressAll(&g_stop_word);
return TRUE;
}
Expand Down
Loading
Loading