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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<F>`, `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
Expand Down
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.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"]
Expand Down
65 changes: 65 additions & 0 deletions src/library.cppm
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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<int(int)>("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 <class F>
[[nodiscard]] F* function(std::string_view name) const {
return reinterpret_cast<F*>(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<handle> 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<handle> 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);

// 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;

} // namespace mcpplibs::os::library
32 changes: 32 additions & 0 deletions src/openkal/library.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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<handle> open(const std::filesystem::path&) {
return std::unexpected(error{errc::not_supported, "loading a library in an ok image", 0});
}

std::optional<handle> 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
1 change: 1 addition & 0 deletions src/os.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
69 changes: 69 additions & 0 deletions src/posix/library.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// mcpplibs.os.library on Linux and macOS: the dynamic loader of the C library.
module;
#include <dlfcn.h>

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());
}

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<handle> 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();
return std::unexpected(error{errc::not_found, std::format("loading {}: {}", name, why ? why : "not loaded"), 0});
}
return handle(h, true);
}

std::optional<handle> 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
72 changes: 72 additions & 0 deletions src/windows/library.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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<int>(utf8.size()), nullptr, 0);
std::wstring out(static_cast<std::size_t>(n > 0 ? n : 0), L'\0');
if (n > 0) MultiByteToWideChar(65001, 0, utf8.data(), static_cast<int>(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<handle> 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<int>(GetLastError())});
return handle(h, true);
}

std::optional<handle> 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<std::size_t>(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;
}

bool supported() noexcept { return true; }

std::string_view suffix() noexcept { return ".dll"; }

} // namespace mcpplibs::os::library
5 changes: 5 additions & 0 deletions src/windows/win32.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
42 changes: 42 additions & 0 deletions tests/library_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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 (!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.
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<unsigned long()>(function)() != 0, "its function is called");
else check(lib->function<int()>(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;
}
Loading