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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ jobs:
run: |
profile='${{ matrix.profile }}'
mcpp test --profile "$profile" 2>&1 | tee tests.log
for name in platform_test fs_test process_test ipc_test unit_test dirs_terminal_test linux_test watch_test; do
for name in platform_test fs_test process_test ipc_test unit_test dirs_terminal_test linux_test watch_test windows_test macos_test; do
grep -q "^$name \.\.\. ok" tests.log || { echo "::error::$name did not run or did not pass"; exit 1; }
done

Expand Down Expand Up @@ -219,7 +219,7 @@ jobs:
run: |
profile='${{ matrix.profile }}'
mcpp test --profile "$profile" ${{ matrix.target }} 2>&1 | tee tests.log
for name in platform_test fs_test process_test ipc_test unit_test dirs_terminal_test linux_test library_test watch_test; do
for name in platform_test fs_test process_test ipc_test unit_test dirs_terminal_test linux_test library_test watch_test windows_test macos_test; do
grep -q "^$name \.\.\. ok" tests.log || { echo "::error::$name did not run or did not pass"; exit 1; }
done

Expand Down Expand Up @@ -271,7 +271,7 @@ jobs:
run: |
set -euo pipefail
mcpp test --target x86_64-openkal 2>&1 | tee tests.log
for name in platform_test fs_test process_test ipc_test unit_test dirs_terminal_test linux_test watch_test; do
for name in platform_test fs_test process_test ipc_test unit_test dirs_terminal_test linux_test watch_test windows_test macos_test; do
grep -q "^$name \.\.\. ok" tests.log || { echo "::error::$name did not pass as an image"; exit 1; }
done
mcpp test --target aarch64-openkal --no-run
Expand Down Expand Up @@ -424,7 +424,7 @@ jobs:
run: |
set -euo pipefail
mcpp test --target wasm32-emscripten --profile '${{ matrix.profile }}' 2>&1 | tee tests.log
for name in platform_test fs_test dirs_terminal_test linux_test library_test watch_test; do
for name in platform_test fs_test dirs_terminal_test linux_test library_test watch_test windows_test macos_test; do
grep -q "^$name \.\.\. ok" tests.log || { echo "::error::$name did not run or did not pass"; exit 1; }
done
- name: The compile-time constants of Emscripten (R3)
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## 0.4.4

- **The system's object models: `windows::com` and `macos::objc`.** A system's
services that are objects rather than functions --- toast notifications, the
shell's dialogs, the Windows Runtime's classes; Foundation, AppKit,
UserNotifications --- are reached through them. On Windows: an apartment, one
reference to an object (`object`, AddRef and Release paired), a method called
by its slot, `query`, `create`, `hstring` (UTF-8 there and back), `activate`,
`factory`, and `delegate`, an object of the program's own that the system
calls back (IUnknown, IAgileObject and an interface whose method is
`Invoke(sender, args)`); an HRESULT becomes an `errc`. On macOS: `id` (one
reference), `send<R>` (objc_msgSend typed as the method), an autorelease pool,
classes and selectors by name, frameworks loaded by name, NSString from and to
UTF-8, a class defined at run time (a delegate), and `block`, a block the
runtime holds and releases. Both are platform modules (R2, R5): declared on
every target, defined on their own platform, not defined in an image, which
loads no library of the system. The libraries are found when first used
(ole32, combase; libobjc, libSystem's blocks through `os::library`), so
nothing is added to a program's link. The tests make a Windows Runtime
`Uri`, call its methods, refuse an interface it lacks and are called back by
a delegate; on macOS they send NSProcessInfo a message, define a class that
receives messages and enumerate an NSArray with a block, each released after;
strings in four scripts go through the system and back on both. The R5 check
covers a member template in a discarded branch.

## 0.4.3

- **openkal 0.25 and the implementations of its family are admitted.** The
Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ dropped (0.2.8).
| `mcpplibs.os.machine` | the machine's architecture (through Rosetta and WoW64), memory and the memory available now, processors (logical, physical, of differing speed) |
| `mcpplibs.os.random` | bytes from the system's source of unpredictable bytes; `hex` |
| `mcpplibs.os.library` | shared libraries at run time: `open` (a path or a name the system searches for), `loaded` (one the process has, without loading it), a handle's `symbol` / `function<F>`, `process_symbol` (as the loader resolves one), `suffix` |
| `mcpplibs.os.linux` / `.macos` / `.windows` / `.posix` | platform modules: declared on every target, defined on their own and, where an image can answer, in an image |
| `mcpplibs.os.linux` / `.macos` / `.windows` / `.posix` | platform modules: declared on every target, defined on their own and, where an image can answer, in an image; the system's object models, through which its services that are not functions are reached: `windows::com` (COM and the Windows Runtime) and `macos::objc` (the Objective-C runtime) |

## Rules

Expand Down Expand Up @@ -185,12 +185,33 @@ would name another system's module on every build. The platform modules in an im
| `posix::effective_user()` | `/proc/self/status` on Linux, `/usr/bin/id -u` on macOS; the largest number when neither answers |
| `macos::translated()` | `false`: macOS runs images through an aarch64 okrun only |
| `windows::code_page()`, `console_output_code_page()`, `long_paths_enabled()`, `machine_registry_path()`, `writable_fixed_drive()`, `posix::set_creation_mask()` | not defined: okrun carries an image's streams and names, and the others belong to a native installation |
| `windows::com`, `macos::objc` | not defined: an image loads no library of the system; ask `if constexpr (os::is_windows)` / `os::is_macos` |

The system's programs are started by absolute name (`%SystemRoot%\System32`, `/usr/bin`), with a
bound of ten seconds, and each answer is asked once per process. `host()` decides which system's
question applies, which is that system's convention; whether a program can be started is
openkal's capability and is answered by the start itself (openkal SPEC clause 30).

## The system's object models (0.4.4)

Some of a system's services are not functions but objects: the shell's dialogs, notifications,
the Windows Runtime's classes, AppKit and the frameworks above Foundation. `windows::com` and
`macos::objc` are how they are reached, and nothing more: a program states the interfaces it
calls (a method by its slot in COM, a message by its selector and its types in Objective-C), as
it would from the system's headers, which this package does not include. What a service *is*
belongs to the library above (a notification's contract, a file dialog's).

| `windows::com` | `macos::objc` |
|---|---|
| `apartment::enter` (CoInitializeEx), `object` (one reference: AddRef and Release paired), `object::call(slot, args...)`, `query` (QueryInterface), `create` (CoCreateInstance) | `id` (one reference: objc_retain and objc_release paired), `send<R>(receiver, selector, args...)` (objc_msgSend typed as the method is), `autorelease_pool`, `class_named`, `selector` |
| `hstring` (UTF-8 there and back, R7), `activate` (RoActivateInstance), `factory` (RoGetActivationFactory) | `string` and `utf8` (NSString, R7), `load_framework` |
| `delegate(interface, invoked)`: an object the system calls back (`Invoke(sender, args)`, the shape of every Windows Runtime event handler), agile | `define_class` (a delegate's methods), `block(function)`: a block the runtime holds, released with its last reference |
| `id("…")`: an interface's identity at compile time; `checked(HRESULT)`: E_NOINTERFACE is `not_supported`, a class not registered `not_found` | `available()`: `not_supported` in a program that loads no library |

The libraries are loaded when first used (ole32 and combase; libobjc and libSystem's blocks,
through `os::library`), so a program links nothing for them and a toolchain without the
system's import libraries builds them unchanged.

## As an ok image

Built for `<arch>-openkal` (openkal SPEC clause 12), a program over this library
Expand Down
3 changes: 3 additions & 0 deletions checks/discarded/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ int main() {
if constexpr (os::is_linux) n += static_cast<int>(os::linux::translator().has_value());
if constexpr (os::is_macos) n += static_cast<int>(os::macos::translated());
if constexpr (os::is_windows) n += static_cast<int>(os::windows::elevated());
// A member template too (0.4.4): its definition is not instantiated where it is not used.
if constexpr (os::is_windows) n += static_cast<int>(os::windows::com::object{}.call(3, 0).has_value());
if constexpr (os::is_macos) n += static_cast<int>(os::macos::objc::send<int>(nullptr, "hash"));
// The same calls in the form that also holds in an image (0.4.0).
if constexpr (os::can_run_on(os::platform::linux))
if (os::host() == os::platform::linux) n += static_cast<int>(os::linux::translator().has_value());
Expand Down
6 changes: 6 additions & 0 deletions checks/emscripten/tests/macos_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// macos_test.cpp of mcpplibs.os, compiled for Emscripten. The imports are restated
// because the dependency scan reads this file and not the one it includes.
import std;
import mcpplibs.os;
import mcpplibs.mtest;
#include "../../../tests/macos_test.cpp"
6 changes: 6 additions & 0 deletions checks/emscripten/tests/windows_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// windows_test.cpp of mcpplibs.os, compiled for Emscripten. The imports are restated
// because the dependency scan reads this file and not the one it includes.
import std;
import mcpplibs.os;
import mcpplibs.mtest;
#include "../../../tests/windows_test.cpp"
6 changes: 6 additions & 0 deletions checks/hosted/tests/macos_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// macos_test.cpp of mcpplibs.os, compiled in this arrangement. The imports are restated
// because the dependency scan reads this file and not the one it includes.
import std;
import mcpplibs.os;
import mcpplibs.mtest;
#include "../../../tests/macos_test.cpp"
6 changes: 6 additions & 0 deletions checks/hosted/tests/windows_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// windows_test.cpp of mcpplibs.os, compiled in this arrangement. The imports are restated
// because the dependency scan reads this file and not the one it includes.
import std;
import mcpplibs.os;
import mcpplibs.mtest;
#include "../../../tests/windows_test.cpp"
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "os"
version = "0.4.3"
version = "0.4.4"
description = "mcpplibs.os: a C++ operating-system library over openkal --- processes, files, environment, terminals, directories and local IPC, with compile-time platform constants and a few platform modules."
license = "Apache-2.0"
authors = ["mcpplibs"]
Expand Down
134 changes: 134 additions & 0 deletions src/macos.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
export module mcpplibs.os.macos;

import std;
import mcpplibs.os.error;

export namespace mcpplibs::os::macos {

Expand All @@ -11,3 +12,136 @@ export namespace mcpplibs::os::macos {
bool translated();

} // namespace mcpplibs::os::macos

// THE OBJECTIVE-C RUNTIME (0.4.4): how the system's frameworks are called
// (Foundation, AppKit, UserNotifications). No other system has it (R2).
// libobjc and the frameworks are loaded when first used (`os::library'), so a
// program links nothing for them; an image loads no library of the system,
// and these are not defined there (R5): a program that is to run as an image
// asks `if constexpr (os::is_macos)', the question of a native build.
//
// objc::autorelease_pool pool;
// void* info = objc::send<void*>(objc::class_named("NSProcessInfo"), "processInfo");
// std::string name = objc::utf8(objc::send<void*>(info, "processName"));
//
// A message is objc_msgSend typed as the method is: `send<R>(receiver,
// selector, args...)'. A result returned in memory rather than in registers
// (a structure larger than two words on x86-64) is not sent this way.
export namespace mcpplibs::os::macos::objc {

// One reference to an object, released with this and retained by a copy
// (objc_retain and objc_release are paired here, R6).
class id {
public:
id() noexcept = default;
id(const id& other) noexcept;
id(id&& other) noexcept : raw_(std::exchange(other.raw_, nullptr)) {}
id& operator=(id other) noexcept; // out of line: it releases, defined on macOS only (R5)
~id();

// A reference the caller owns (what alloc, new, copy and init return) becomes this one.
static id adopt(void* raw) noexcept;
// A reference of its own to an object another holds (an autoreleased result).
static id borrow(void* raw) noexcept;

[[nodiscard]] void* raw() const noexcept { return raw_; }
explicit operator bool() const noexcept { return raw_ != nullptr; }

private:
void* raw_ = nullptr;
};

// Whether the runtime is here, with Foundation (NSString, NSArray, ...):
// not_supported where this program cannot load a library (a static program).
result<void> available();
// A framework of the system, by name ("UserNotifications"): its classes exist after.
result<void> load_framework(std::string_view name);
// A class by name (null: none), a selector by name, objc_msgSend (null: no runtime).
[[nodiscard]] void* class_named(std::string_view name);
[[nodiscard]] void* selector(std::string_view name);
[[nodiscard]] void* message_function();

// A message to `receiver'; with no runtime, nothing is sent and R's empty value returned.
template <class R = void, class... A>
R send(void* receiver, std::string_view name, A... args) {
using function = R (*)(void*, void*, A...);
void* const sender = message_function();
if (!sender) {
if constexpr (std::is_void_v<R>) return;
else return R{};
}
return reinterpret_cast<function>(sender)(receiver, selector(name), args...);
}
template <class R = void, class... A>
R send(const id& receiver, std::string_view name, A... args) {
return send<R>(receiver.raw(), name, args...);
}

// The objects autoreleased while this lives are released with it.
class autorelease_pool {
public:
autorelease_pool();
autorelease_pool(const autorelease_pool&) = delete;
autorelease_pool& operator=(const autorelease_pool&) = delete;
~autorelease_pool();

private:
void* token_ = nullptr;
};

// An NSString from UTF-8, and the UTF-8 of one (R7).
[[nodiscard]] id string(std::string_view utf8);
[[nodiscard]] std::string utf8(void* string);
[[nodiscard]] std::string utf8(const id& string);

// A class made here (a delegate): NSObject's or another's subclass with these
// methods. A method is its selector, a function `R(void* self, void* selector,
// A...)' and its type encoding ("v@:q"). A protocol the runtime does not know
// is passed over (a delegate is asked by its methods). A name already taken:
// exists.
struct method {
std::string_view selector;
void* implementation;
std::string_view types;
};
result<void*> define_class(std::string_view name, std::string_view superclass, std::span<const method> methods,
std::span<const std::string_view> protocols = {});

namespace detail {
// What a block holds: the function, counted across the runtime's copies of the block.
struct block_body {
std::atomic<std::uint32_t> references{1};
void (*destroy)(block_body*) = nullptr;
};
// The block's layout (the Blocks ABI): a literal, then what it captures.
struct block_literal {
void* isa;
std::int32_t flags;
std::int32_t reserved;
void* invoke;
const void* descriptor;
block_body* body;
};
id make_block(void* invoke, block_body* body);
} // namespace detail

// A block (a completion handler, an enumeration's body) that runs `body': a
// copy the runtime holds, which the receiver may keep; `body' goes with the
// last reference to it. It carries no signature (BLOCK_HAS_SIGNATURE), so a
// receiver that reads one (NSInvocation) does not take it; one that calls it,
// as a completion handler is called, does.
template <class R, class... A>
id block(std::function<R(A...)> body) {
struct typed : detail::block_body {
std::function<R(A...)> function;
};
auto* held = new typed{};
held->function = std::move(body);
held->destroy = [](detail::block_body* self) { delete static_cast<typed*>(self); };
R (*invoke)(detail::block_literal*, A...) = [](detail::block_literal* self, A... args) -> R {
return static_cast<typed*>(self->body)->function(args...);
};
return detail::make_block(reinterpret_cast<void*>(invoke), held);
}

} // namespace mcpplibs::os::macos::objc
Loading
Loading