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
27 changes: 26 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ on:
default: ""
env:
MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }}
MCPP_VERSION: 2026.8.26.2
MCPP_VERSION: 2026.8.27.1
XLINGS_VERSION: v2026.8.17.2
XLINGS_NON_INTERACTIVE: '1'

Expand All @@ -49,6 +49,31 @@ jobs:
# The specification is checked out at the branch under test where it has
# one, so that this run asserts what it is for: that the specification as
# written there and this implementation as written here agree today.
# THE COMMITTED MANIFEST NAMES NO DIRECTORY OF ANYBODY'S MACHINE.
#
# Two scripts in the specification's repository rewrite this manifest to
# name a working tree --- run-conformance.sh and run-kit-tests.sh --- and
# both restore it through a trap. A trap does not fire when the process is
# killed, and a run by hand followed by `git add -A` then publishes a path
# that exists on one machine: a consumer resolving from the index is handed
# a manifest pointing at a directory that exists nowhere.
#
# ⚠️ THAT HAS HAPPENED IN THIS ECOSYSTEM, in openkal-musl, and the working
# tree here has carried the same rewrite more than once since. This step
# runs first, so what it examines is what the commit contains.
- name: The committed manifest names no local directory
run: |
set -euo pipefail
bad=$(grep -nE '^[a-z-]+ = \{[^}]*path = "(/|[A-Za-z]:)' mcpp.toml || true)
if [ -n "$bad" ]; then
echo "::error::the committed manifest names an absolute path"
printf '%s\n' "$bad" | sed 's/^/ /'
echo " run 'git checkout -- mcpp.toml' after using the"
echo " specification's conformance or kit scripts by hand."
exit 1
fi
echo " ok every dependency is named by version, branch or a relative path"

- name: The specification
run: |
git clone --quiet https://github.com/mcpplibs/openkal.git .spec
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ compile_commands.json
# What a system leaves behind.
.DS_Store
Thumbs.db

# The specification tree tools/run-conformance.sh clones beside the sources.
.spec/
4 changes: 2 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "openkal-linux"
version = "0.5.4"
version = "0.6.0"
description = "The reference implementation of openkal for Linux, written on the kernel's own system-call interface so that it can be placed beneath a C library as well as above one."
license = "Apache-2.0"

Expand All @@ -18,7 +18,7 @@ authors = ["mcpplibs"]
repo = "https://github.com/mcpplibs/openkal-linux"

[dependencies]
openkal = "0.7.0"
openkal = "0.8.0"

# The package contributes definitions and no modules. The interface it
# implements is declared by the specification package, which this package
Expand Down
151 changes: 151 additions & 0 deletions src/datagram.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#include "sys.h"
#include "handle.h"
#include "endpoint.h"
#include <openkal/datagram.h>

// openkal.datagram upon the kernel's socket calls.
//
// A DATAGRAM IS NOT PACKED AS A kal_stream, and the handle type is its own for
// that reason: kal_stream_read reports a count and not a boundary, so reading a
// datagram through it would lose the property that distinguishes this interface.
// The packing is the same, the type is not, and the type is what prevents the
// mistake.

namespace {

int fd_of(kal_datagram d) { return okl::unpack(d.h); }

} // namespace

extern "C" {

int kal_datagram_open(const kal_endpoint* local, kal_datagram* out) {
if (out == nullptr) return kal_err_invalid;

// A null local endpoint asks for one that may send and whose receiving
// address is unspecified. IPv4 is chosen for it, because a family must be
// named at the point the socket is made and this is the one every
// environment that has a network at all provides.
okl_long family = okl::af_inet;
if (local != nullptr) {
family = okl::family_of(*local);
if (family < 0) return kal_err_invalid;
}

const okl_long fd = okl::sys(okl::nr_socket, family,
okl::sock_dgram | okl::sock_cloexec,
okl::ipproto_udp);
if (okl::failed(fd)) return okl::translate(fd);

if (local != nullptr) {
okl::ksockaddr_storage ss{};
okl_long len = 0;
if (const int rc = okl::to_kernel(*local, ss, len); rc != kal_ok) {
okl::sys(okl::nr_close, fd);
return rc;
}
if (const okl_long r = okl::sys(okl::nr_bind, fd,
reinterpret_cast<okl_long>(&ss), len);
okl::failed(r)) {
okl::sys(okl::nr_close, fd);
return okl::translate(r);
}
}

out->h = okl::pack(static_cast<int>(fd));
if (out->h == 0) { okl::sys(okl::nr_close, fd); return kal_err_no_memory; }
return kal_ok;
}

int kal_datagram_local(kal_datagram d, kal_endpoint* out) {
if (out == nullptr) return kal_err_invalid;
const int fd = fd_of(d);
if (fd < 0) return kal_err_invalid;

okl::ksockaddr_storage ss{};
okl_long len = static_cast<okl_long>(sizeof ss);
const okl_long r = okl::sys(okl::nr_getsockname, fd,
reinterpret_cast<okl_long>(&ss),
reinterpret_cast<okl_long>(&len));
if (okl::failed(r)) return okl::translate(r);
return okl::from_kernel(ss, *out);
}

kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len,
const kal_endpoint* to) {
const int fd = fd_of(d);
if (fd < 0 || to == nullptr) return { 0, kal_err_invalid };

okl::ksockaddr_storage ss{};
okl_long addrlen = 0;
if (const int rc = okl::to_kernel(*to, ss, addrlen); rc != kal_ok)
return { 0, rc };

for (;;) {
const okl_long r = okl::sys(okl::nr_sendto, fd,
reinterpret_cast<okl_long>(buf),
static_cast<okl_long>(len), 0,
reinterpret_cast<okl_long>(&ss), addrlen);
if (okl::interrupted(r)) continue;
if (okl::failed(r)) return { 0, okl::translate(r) };

// A MESSAGE IS SENT WHOLE OR NOT AT ALL, which is what this interface
// states. The kernel reports a count anyway; a count short of the length
// would mean the medium had split the message, which for a datagram
// socket it does not do. Reporting the short count as success would give
// a caller a partial send this interface says cannot occur, so it is
// reported as a failure of the medium instead.
const kal_uintptr n = static_cast<kal_uintptr>(r);
return { n, n == len ? kal_ok : kal_err_io };
}
}

kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len,
kal_endpoint* from) {
const int fd = fd_of(d);
if (fd < 0) return { 0, kal_err_invalid };

okl::ksockaddr_storage ss{};
okl_long addrlen = static_cast<okl_long>(sizeof ss);

for (;;) {
const okl_long r = okl::sys(okl::nr_recvfrom, fd,
reinterpret_cast<okl_long>(buf),
static_cast<okl_long>(len), 0,
reinterpret_cast<okl_long>(&ss),
reinterpret_cast<okl_long>(&addrlen));
if (okl::interrupted(r)) continue;
if (okl::failed(r)) return { 0, okl::translate(r) };

// THE COUNT REPORTED IS WHAT WAS PLACED IN THE BUFFER, not what was
// sent. Without MSG_TRUNC the kernel already reports the former, which
// is what this interface requires: a caller that trusted the larger
// number would read beyond its own buffer.
if (from != nullptr) {
// A sender whose family this implementation does not know leaves the
// endpoint zeroed rather than partly filled. The transfer still
// happened and is reported; what is unknown is who sent it.
if (okl::from_kernel(ss, *from) != kal_ok) {
for (auto& b : from->addr) b = 0;
from->addr_len = 0;
from->port = 0;
}
}
return { static_cast<kal_uintptr>(r), kal_ok };
}
}

void kal_datagram_close(kal_datagram d) {
const int fd = fd_of(d);
if (fd < 0) return;
okl::sys(okl::nr_close, fd);
okl::retire(d.h);
}

// Broadcast is not claimed. The kernel provides it only after SO_BROADCAST has
// been set, and this interface has no operation that would set it; a word
// claiming a facility no operation reaches is the disagreement clause 6.2 exists
// to prevent.
const kal_uintptr kal_datagram_props = KAL_DGRAM_PROP_IPV6;

} // extern "C"
112 changes: 112 additions & 0 deletions src/endpoint.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Conversion between kal_endpoint and the kernel's socket address structures.
//
// SHARED BY openkal.net AND openkal.datagram BECAUSE THE TYPE IS. Either
// interface may be provided without the other, so the conversion belongs to
// neither; writing it twice would be one decision stated in two places, and the
// two would eventually disagree about which lengths are accepted.
#pragma once
#include "sys.h"
#include <openkal/types.h>

namespace okl {

// The port is carried in host order by kal_endpoint and in network order by the
// kernel. The conversion is written out rather than taken from a C library's
// htons, for the reason the head of sys.h gives.
inline unsigned short to_net_port(kal_u32 port) {
const unsigned short p = static_cast<unsigned short>(port & 0xffffu);
return static_cast<unsigned short>((p << 8) | (p >> 8));
}
inline kal_u32 from_net_port(unsigned short net) {
return static_cast<kal_u32>((net << 8) | (net >> 8)) & 0xffffu;
}

// Fills a kernel address from an endpoint, and reports its length.
//
// A LENGTH THIS IMPLEMENTATION DOES NOT KNOW IS REFUSED RATHER THAN READ AS ONE
// IT DOES. The specification defines the set of lengths and allows it to grow;
// an implementation that ignored the field would misread every address a later
// revision defines, and would do so silently.
inline int to_kernel(const kal_endpoint& ep, ksockaddr_storage& out, okl_long& len) {
for (auto& b : out.pad) b = 0;

if (ep.addr_len == 4) {
auto* v4 = reinterpret_cast<ksockaddr_in*>(&out);
v4->family = af_inet;
v4->port = to_net_port(ep.port);
okl_u32 a = 0;
for (int i = 0; i < 4; ++i)
a |= static_cast<okl_u32>(ep.addr[i]) << (i * 8); // already network order
v4->addr = a;
for (auto& z : v4->zero) z = 0;
len = static_cast<okl_long>(sizeof(ksockaddr_in));
return kal_ok;
}

// Sixteen bytes is an address; twenty is an address followed by a scope
// identifier, which is carried in the four bytes after it.
if (ep.addr_len == 16 || ep.addr_len == 20) {
auto* v6 = reinterpret_cast<ksockaddr_in6*>(&out);
v6->family = af_inet6;
v6->port = to_net_port(ep.port);
v6->flowinfo = 0;
for (int i = 0; i < 16; ++i) v6->addr[i] = ep.addr[i];
okl_u32 scope = 0;
if (ep.addr_len == 20)
for (int i = 0; i < 4; ++i)
scope |= static_cast<okl_u32>(ep.addr[16 + i]) << (i * 8);
v6->scope_id = scope;
len = static_cast<okl_long>(sizeof(ksockaddr_in6));
return kal_ok;
}

return kal_err_invalid;
}

// Fills an endpoint from a kernel address. A family this implementation does
// not know leaves the endpoint zeroed and reports it, for the same reason.
inline int from_kernel(const ksockaddr_storage& in, kal_endpoint& out) {
for (auto& b : out.addr) b = 0;
out.addr_len = 0;
out.port = 0;

if (in.family == af_inet) {
const auto* v4 = reinterpret_cast<const ksockaddr_in*>(&in);
const okl_u32 a = v4->addr;
for (int i = 0; i < 4; ++i)
out.addr[i] = static_cast<kal_u8>((a >> (i * 8)) & 0xffu);
out.addr_len = 4;
out.port = from_net_port(v4->port);
return kal_ok;
}

if (in.family == af_inet6) {
const auto* v6 = reinterpret_cast<const ksockaddr_in6*>(&in);
for (int i = 0; i < 16; ++i) out.addr[i] = v6->addr[i];
// A zero scope identifier is reported as the shorter form. The two
// lengths denote the same address when the scope is zero, and reporting
// the shorter one keeps an address that came in as sixteen bytes going
// back out as sixteen.
if (v6->scope_id == 0) {
out.addr_len = 16;
} else {
for (int i = 0; i < 4; ++i)
out.addr[16 + i] = static_cast<kal_u8>((v6->scope_id >> (i * 8)) & 0xffu);
out.addr_len = 20;
}
out.port = from_net_port(v6->port);
return kal_ok;
}

return kal_err_invalid;
}

// Which socket family an endpoint asks for, or -1 for a length that is not one
// of the defined ones.
inline okl_long family_of(const kal_endpoint& ep) {
if (ep.addr_len == 4) return af_inet;
if (ep.addr_len == 16 || ep.addr_len == 20) return af_inet6;
return -1;
}

} // namespace okl
Loading
Loading