From c54c97a063986e6bad94f37a290fb4a184cd7701 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 27 Oct 2026 14:50:31 +0900 Subject: [PATCH 1/2] 0.1.2: shared libraries at run time (mcpplibs.os.library) A plugin system loads a module by name, and an extension that another program loads needs that program's functions, which it did not link against (a Python extension and the interpreter's C API). Both need the dynamic loader, which openkal does not cover: it belongs to the C library on Linux and macOS and to kernel32 on Windows. The new module mcpplibs.os.library: - open(path or name) answers a handle that closes the library when it goes; the library's symbols stay local to it; - loaded(name) finds a library the process already has, without loading it; - handle::symbol and function look up what a library defines; - process_symbol(name) resolves a symbol as the loader does: the global scope on POSIX, every module of the process on Windows; - suffix() is the system's library suffix. The implementations are src/posix/library.cpp (dlopen), src/windows/library.cpp (kernel32, declared in win32.h) and src/openkal/library.cpp (an image loads nothing). library_test opens a system library by name, calls one of its functions, finds a loaded library and a program symbol, and gets an error for a missing library. --- README.md | 6 ++-- mcpp.toml | 2 +- src/library.cppm | 61 +++++++++++++++++++++++++++++++++++ src/openkal/library.cpp | 30 ++++++++++++++++++ src/os.cppm | 1 + src/posix/library.cpp | 58 ++++++++++++++++++++++++++++++++++ src/windows/library.cpp | 70 +++++++++++++++++++++++++++++++++++++++++ src/windows/win32.h | 5 +++ tests/library_test.cpp | 40 +++++++++++++++++++++++ 9 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 src/library.cppm create mode 100644 src/openkal/library.cpp create mode 100644 src/posix/library.cpp create mode 100644 src/windows/library.cpp create mode 100644 tests/library_test.cpp diff --git a/README.md b/README.md index 7167b3f..41f8507 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,9 @@ int main() { | a program | its domain | — | The kernel layer is openkal and only openkal. Native code in this package serves only -what is not a kernel question: the machine beneath the system (`machine`), and the platform -modules. +what is not a kernel question: the machine beneath the system (`machine`), the dynamic +loader (`library`: the C library's on Linux and macOS, kernel32's on Windows), and the +platform modules. ## Modules @@ -50,6 +51,7 @@ modules. | `mcpplibs.os.net` | loopback TCP | | `mcpplibs.os.machine` | the machine's architecture (through Rosetta and WoW64), memory | | `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`, `process_symbol` (as the loader resolves one), `suffix` | | `mcpplibs.os.linux` / `.macos` / `.windows` / `.posix` | platform modules: declared on every target, defined only on their own | ## Rules diff --git a/mcpp.toml b/mcpp.toml index 4514942..2611e20 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "os" -version = "0.1.1" +version = "0.1.2" 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"] diff --git a/src/library.cppm b/src/library.cppm new file mode 100644 index 0000000..3512055 --- /dev/null +++ b/src/library.cppm @@ -0,0 +1,61 @@ +// mcpplibs.os.library --- shared libraries the program loads at run time, and +// the symbols the program and its libraries define. +// +// auto lib = os::library::open("plugins/search.so"); // or a name the system searches for +// auto f = lib->function("square"); // nothing when it defines none +// +// What a plugin system needs (a module a program loads by name), and what an +// extension loaded by another program needs: the functions of the program +// that loaded it (`process_symbol', `loaded'), which it did not link against. +// A library stays loaded while a handle to it is open; `loaded' does not load +// one and does not keep it. +export module mcpplibs.os.library; + +import std; +import mcpplibs.os.error; + +export namespace mcpplibs::os::library { + +class handle { +public: + handle() noexcept = default; + handle(handle&& other) noexcept : native_(std::exchange(other.native_, nullptr)), owned_(std::exchange(other.owned_, false)) {} + handle& operator=(handle&& other) noexcept; + handle(const handle&) = delete; + handle& operator=(const handle&) = delete; + ~handle(); + + // The address of a symbol the library defines; nothing when it defines none. + [[nodiscard]] void* symbol(std::string_view name) const; + template + [[nodiscard]] F* function(std::string_view name) const { + return reinterpret_cast(symbol(name)); + } + [[nodiscard]] bool valid() const noexcept { return native_ != nullptr; } + + // For the implementations. + handle(void* native, bool owned) noexcept : native_(native), owned_(owned) {} + +private: + void* native_ = nullptr; + bool owned_ = false; // opened here: closed with the handle +}; + +// Loads a library: a path, or a bare name (`libz.so.1', `zlib1.dll') the +// system searches for as it does for a program's own. Its symbols are its +// own: they do not join the program's (RTLD_LOCAL). +result open(const std::filesystem::path& file); + +// A library already in the process, by the name it was loaded under +// (`python3.dll', `libpython3.12.so.1.0'); nothing when it is not loaded. +std::optional loaded(std::string_view name); + +// A symbol of the program as the loader resolves one: the executable, then +// every library loaded into the process's global scope (on Windows, every +// module of the process, in load order). Nothing when none defines it. +[[nodiscard]] void* process_symbol(std::string_view name); + +// The suffix of a shared library of this system: `.so', `.dylib', `.dll'. +[[nodiscard]] std::string_view suffix() noexcept; + +} // namespace mcpplibs::os::library diff --git a/src/openkal/library.cpp b/src/openkal/library.cpp new file mode 100644 index 0000000..2625fed --- /dev/null +++ b/src/openkal/library.cpp @@ -0,0 +1,30 @@ +// mcpplibs.os.library in an ok image: an image is linked whole, and openkal +// loads no library into it (SPEC clause 12), so there is nothing to open. +module mcpplibs.os.library; + +import std; +import mcpplibs.os.error; + +namespace mcpplibs::os::library { + +handle& handle::operator=(handle&& other) noexcept { + native_ = std::exchange(other.native_, nullptr); + owned_ = std::exchange(other.owned_, false); + return *this; +} + +handle::~handle() = default; + +void* handle::symbol(std::string_view) const { return nullptr; } + +result open(const std::filesystem::path&) { + return std::unexpected(error{errc::not_supported, "loading a library in an ok image", 0}); +} + +std::optional loaded(std::string_view) { return std::nullopt; } + +void* process_symbol(std::string_view) { return nullptr; } + +std::string_view suffix() noexcept { return ""; } + +} // namespace mcpplibs::os::library diff --git a/src/os.cppm b/src/os.cppm index 163cfd9..d9f7fcd 100644 --- a/src/os.cppm +++ b/src/os.cppm @@ -19,6 +19,7 @@ export import mcpplibs.os.ipc; export import mcpplibs.os.net; export import mcpplibs.os.machine; export import mcpplibs.os.random; +export import mcpplibs.os.library; export import mcpplibs.os.linux; export import mcpplibs.os.macos; export import mcpplibs.os.windows; diff --git a/src/posix/library.cpp b/src/posix/library.cpp new file mode 100644 index 0000000..c2cc7ba --- /dev/null +++ b/src/posix/library.cpp @@ -0,0 +1,58 @@ +// mcpplibs.os.library on Linux and macOS: the dynamic loader of the C library. +module; +#include + +module mcpplibs.os.library; + +import std; +import mcpplibs.os.error; +import mcpplibs.os.fs; +import mcpplibs.os.platform; + +namespace mcpplibs::os::library { + +handle& handle::operator=(handle&& other) noexcept { + if (this != &other) { + if (owned_ && native_) ::dlclose(native_); + native_ = std::exchange(other.native_, nullptr); + owned_ = std::exchange(other.owned_, false); + } + return *this; +} + +handle::~handle() { + if (owned_ && native_) ::dlclose(native_); +} + +void* handle::symbol(std::string_view name) const { + if (!native_) return nullptr; + const std::string n(name); + return ::dlsym(native_, n.c_str()); +} + +result open(const std::filesystem::path& file) { + const std::string name = fs::utf8(file); + void* h = ::dlopen(name.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!h) { + const char* why = ::dlerror(); + return std::unexpected(error{errc::not_found, std::format("loading {}: {}", name, why ? why : "not loaded"), 0}); + } + return handle(h, true); +} + +std::optional loaded(std::string_view name) { + const std::string n(name); + void* h = ::dlopen(n.c_str(), RTLD_NOW | RTLD_NOLOAD); + if (!h) return std::nullopt; + // RTLD_NOLOAD counts a reference all the same: this handle gives it back. + return handle(h, true); +} + +void* process_symbol(std::string_view name) { + const std::string n(name); + return ::dlsym(RTLD_DEFAULT, n.c_str()); +} + +std::string_view suffix() noexcept { return is_macos ? ".dylib" : ".so"; } + +} // namespace mcpplibs::os::library diff --git a/src/windows/library.cpp b/src/windows/library.cpp new file mode 100644 index 0000000..8bc5c32 --- /dev/null +++ b/src/windows/library.cpp @@ -0,0 +1,70 @@ +// mcpplibs.os.library on Windows: the loader of kernel32 (src/windows/win32.h). +module; +#include "win32.h" + +module mcpplibs.os.library; + +import std; +import mcpplibs.os.error; +import mcpplibs.os.fs; + +namespace mcpplibs::os::library { + +namespace { + +std::wstring wide(std::string_view utf8) { + const int n = MultiByteToWideChar(65001, 0, utf8.data(), static_cast(utf8.size()), nullptr, 0); + std::wstring out(static_cast(n > 0 ? n : 0), L'\0'); + if (n > 0) MultiByteToWideChar(65001, 0, utf8.data(), static_cast(utf8.size()), out.data(), n); + return out; +} + +} // namespace + +handle& handle::operator=(handle&& other) noexcept { + if (this != &other) { + if (owned_ && native_) FreeLibrary(native_); + native_ = std::exchange(other.native_, nullptr); + owned_ = std::exchange(other.owned_, false); + } + return *this; +} + +handle::~handle() { + if (owned_ && native_) FreeLibrary(native_); +} + +void* handle::symbol(std::string_view name) const { + if (!native_) return nullptr; + const std::string n(name); + return GetProcAddress(native_, n.c_str()); +} + +result open(const std::filesystem::path& file) { + const std::string name = fs::utf8(file); + okos_handle h = LoadLibraryW(wide(name).c_str()); + if (!h) return std::unexpected(error{errc::not_found, std::format("loading {}", name), static_cast(GetLastError())}); + return handle(h, true); +} + +std::optional loaded(std::string_view name) { + // GetModuleHandleW counts no reference: the handle closes nothing. + okos_handle h = GetModuleHandleW(wide(name).c_str()); + if (!h) return std::nullopt; + return handle(h, false); +} + +void* process_symbol(std::string_view name) { + const std::string n(name); + okos_handle modules[1024]; + okos_dword bytes = 0; + if (!K32EnumProcessModules(GetCurrentProcess(), modules, sizeof modules, &bytes)) return nullptr; + const std::size_t count = std::min(bytes / sizeof(okos_handle), std::size(modules)); + for (std::size_t i = 0; i < count; ++i) + if (void* p = GetProcAddress(modules[i], n.c_str())) return p; + return nullptr; +} + +std::string_view suffix() noexcept { return ".dll"; } + +} // namespace mcpplibs::os::library diff --git a/src/windows/win32.h b/src/windows/win32.h index 505bdd9..b1adb48 100644 --- a/src/windows/win32.h +++ b/src/windows/win32.h @@ -29,5 +29,10 @@ __declspec(dllimport) okos_bool __stdcall IsWow64Process2(okos_handle, unsigned // links nothing beyond kernel32 for a question few programs ask. __declspec(dllimport) okos_handle __stdcall LoadLibraryW(const wchar_t*); __declspec(dllimport) void* __stdcall GetProcAddress(okos_handle, const char*); +// mcpplibs.os.library. +__declspec(dllimport) okos_bool __stdcall FreeLibrary(okos_handle); +__declspec(dllimport) okos_handle __stdcall GetModuleHandleW(const wchar_t*); +__declspec(dllimport) okos_dword __stdcall GetLastError(void); +__declspec(dllimport) okos_bool __stdcall K32EnumProcessModules(okos_handle, okos_handle*, okos_dword, okos_dword*); __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned, okos_dword, const char*, int, wchar_t*, int); } diff --git a/tests/library_test.cpp b/tests/library_test.cpp new file mode 100644 index 0000000..608042a --- /dev/null +++ b/tests/library_test.cpp @@ -0,0 +1,40 @@ +// Shared libraries at run time: a system library opened by name and its +// functions called; a library the process already has, found without loading +// it; a symbol of the program; and a library that is not there, an error. +#include "check.hpp" +import std; +import mcpplibs.os; + +namespace os = mcpplibs::os; + +int main() { + if constexpr (os::is_image) { + check(!os::library::open("anything").has_value(), "an image loads no library"); + return g_failures; + } + // A library every process of the system has, and a function of it. + const std::string_view system = os::is_windows ? "kernel32.dll" : os::is_macos ? "/usr/lib/libSystem.B.dylib" : "libc.so.6"; + const std::string_view function = os::is_windows ? "GetCurrentProcessId" : "getpid"; + auto lib = os::library::open(std::filesystem::path(std::string(system))); + check(lib.has_value(), "a system library opens by name"); + if (lib) { + check(lib->symbol(function) != nullptr, "and defines its functions"); + check(lib->symbol("no_such_function_anywhere") == nullptr, "and not others"); + // (x86_64 and aarch64 Windows have one calling convention.) + if constexpr (os::is_windows) check(lib->function(function)() != 0, "its function is called"); + else check(lib->function(function)() > 0, "its function is called"); + } + auto present = os::library::loaded(system); + check(present.has_value() && present->symbol(function) != nullptr, "a library in the process is found without loading it"); + check(!os::library::loaded("libnothing-loaded-here.so.0").has_value(), "and one that is not, is not"); + check(os::library::process_symbol(function) != nullptr, "the program's symbols resolve as the loader resolves them"); + auto missing = os::library::open("libmcpplibs-os-no-such-library.so"); + check(!missing && missing.error().code == os::errc::not_found && !missing.error().message().empty(), "a missing library is an error"); + check(os::library::suffix() == (os::is_windows ? ".dll" : os::is_macos ? ".dylib" : ".so"), "the system's suffix"); + // Moving a handle keeps the library open once. + if (lib) { + os::library::handle moved = std::move(*lib); + check(moved.valid() && !lib->valid(), "a handle moves"); + } + return g_failures; +} From 7305bd084fe7da5d0f9cfce61ce267e96220d487 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 27 Oct 2026 14:54:32 +0900 Subject: [PATCH 2/2] =?UTF-8?q?library:=20supported()=20=E2=80=94=20a=20st?= =?UTF-8?q?atic=20program=20(aarch64=20Linux's=20musl,=20an=20ok=20image)?= =?UTF-8?q?=20loads=20no=20library=20and=20open()=20says=20so?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/library.cppm | 4 ++++ src/openkal/library.cpp | 2 ++ src/posix/library.cpp | 11 +++++++++++ src/windows/library.cpp | 2 ++ tests/library_test.cpp | 6 ++++-- 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/library.cppm b/src/library.cppm index 3512055..1e8a840 100644 --- a/src/library.cppm +++ b/src/library.cppm @@ -55,6 +55,10 @@ std::optional loaded(std::string_view name); // module of the process, in load order). Nothing when none defines it. [[nodiscard]] void* process_symbol(std::string_view name); +// Whether this program can load libraries: not when it is linked statically +// (a static musl program, an ok image), where `open' answers not_supported. +[[nodiscard]] bool supported() noexcept; + // The suffix of a shared library of this system: `.so', `.dylib', `.dll'. [[nodiscard]] std::string_view suffix() noexcept; diff --git a/src/openkal/library.cpp b/src/openkal/library.cpp index 2625fed..f3d9908 100644 --- a/src/openkal/library.cpp +++ b/src/openkal/library.cpp @@ -25,6 +25,8 @@ std::optional loaded(std::string_view) { return std::nullopt; } void* process_symbol(std::string_view) { return nullptr; } +bool supported() noexcept { return false; } + std::string_view suffix() noexcept { return ""; } } // namespace mcpplibs::os::library diff --git a/src/posix/library.cpp b/src/posix/library.cpp index c2cc7ba..cbbda24 100644 --- a/src/posix/library.cpp +++ b/src/posix/library.cpp @@ -30,8 +30,19 @@ void* handle::symbol(std::string_view name) const { return ::dlsym(native_, n.c_str()); } +bool supported() noexcept { + // A static program's loader is a stub: it gives no handle, not even the program's own. + static const bool answer = [] { + void* self = ::dlopen(nullptr, RTLD_NOW); + if (self) ::dlclose(self); + return self != nullptr; + }(); + return answer; +} + result open(const std::filesystem::path& file) { const std::string name = fs::utf8(file); + if (!supported()) return std::unexpected(error{errc::not_supported, std::format("loading {}: the program is linked statically", name), 0}); void* h = ::dlopen(name.c_str(), RTLD_NOW | RTLD_LOCAL); if (!h) { const char* why = ::dlerror(); diff --git a/src/windows/library.cpp b/src/windows/library.cpp index 8bc5c32..b34bfad 100644 --- a/src/windows/library.cpp +++ b/src/windows/library.cpp @@ -65,6 +65,8 @@ void* process_symbol(std::string_view name) { return nullptr; } +bool supported() noexcept { return true; } + std::string_view suffix() noexcept { return ".dll"; } } // namespace mcpplibs::os::library diff --git a/tests/library_test.cpp b/tests/library_test.cpp index 608042a..5221d73 100644 --- a/tests/library_test.cpp +++ b/tests/library_test.cpp @@ -8,8 +8,10 @@ import mcpplibs.os; namespace os = mcpplibs::os; int main() { - if constexpr (os::is_image) { - check(!os::library::open("anything").has_value(), "an image loads no library"); + if (!os::library::supported()) { + // An ok image, or a program linked statically (aarch64 Linux's GCC links musl statically). + auto none = os::library::open("anything"); + check(!none && none.error().code == os::errc::not_supported, "a static program loads no library, and says so"); return g_failures; } // A library every process of the system has, and a function of it.