---
title: "a hacky fucked up way to portably access errno from outside of c"
slug: a-hacky-fucked-up-way-to-portably-access-errno-from-outside-of-c
url: https://listedarticles.com/articles/a-hacky-fucked-up-way-to-portably-access-errno-from-outside-of-c
canonical_url: https://sebsite.pw/w/20260925-errno.html
content_type: blog_post
language: en
published_at: 2026-09-25T00:00:00.000Z
updated_at: 2026-10-22T23:12:49.491Z
authored_by: human
publisher: "sebsite.pw"
publisher_url: https://sebsite.pw/
topics: ["C", "Programming Languages"]
license: all-rights-reserved
word_count: 1389
reading_minutes: 6
citation: "sebsite.pw. \"a hacky fucked up way to portably access errno from outside of c.\" 25 Sept 2026. https://sebsite.pw/w/20260925-errno.html (all-rights-reserved)"
# The full text follows. The web page shows an extract and sends readers
# to the source above; quote the citation and link the canonical URL.
---

# a hacky fucked up way to portably access errno from outside of c

> sebsite shows a cursed but portable way to read errno from Hare without a C wrapper: redirect stderr to a temporary file, call perror, read back the message, and match it against a table of strerror strings to recover the error number.

# a hacky fucked up way to portably access errno from outside of c

2026-09-25

here's something really cursed i made a while back. the goal here is to create a portable way to obtain the value of errno from outside of c, without using a wrapper function written in c.

for those unaware, errno is defined as a macro, and its expansion is non-portable and usually comes with no stability guarantees. so if you're calling libc functions from some other language (such as hare, which i'll be using in this post), it's not possible to retrieve error values from them without relying on an implementation detail or writing a wrapper function in c which returns errno.

...unless you're willing to resort to some fucked up hacks, that is.
preamble

```
use io;
use sys;
use types::c;

@symbol("close") fn c_close(fd: int) int;
@symbol("dup") fn c_dup(oldfd: int) int;
@symbol("dup2") fn c_dup2(oldfd: int, newfd: int) int;
@symbol("lseek") fn c_lseek(fd: int, offs: io::off, whence: int) io::off;
@symbol("mkstemp") fn c_mkstemp(template: *c::char) int;
@symbol("perror") fn c_perror(s: nullable *c::char) void;
@symbol("read") fn c_read(fd: int, buf: *opaque, count: size) c::ssize;
@symbol("strerror") fn c_strerror(errnum: int) *c::char;
@symbol("unlink") fn c_unlink(path: *c::char) int;
@symbol("write") fn c_write(fd: int, buf: *opaque, count: size) c::ssize;

def SEEK_SET = 0;
def STDERR_FILENO = 2i;
```

there aren't any libc functions which return the value of errno. there are, however, functions which return a *string* that describes errno. so let's start there.

```
export type error = !str;
```

standard c defines two promising functions: `strerror` and `perror.` posix defines two more functions: `strerror_r` and `strerror_l`. unfortunately, the strerror functions all take in the error number as an argument. only `perror` reads from errno directly. `perror` prints an error string to stderr, so our goal now to is to somehow capture the error string from `perror`, without actually writing anything to stderr.

alright, so let's do it. during program initialization, i dup `STDERR_FILENO` (2), open a temporary file, and change `STDERR_FILENO` to refer to the temporary file:

```
let stderr = -1;
let tmpfd = -1;
let tmpname: [_]u8 = ['/', 't', 'm', 'p', '/', 'X', 'X', 'X', 'X', 'X', 'X', '\0'];

@init fn stderr_tmp() void = {
	stderr = c_dup(STDERR_FILENO);
	assert(stderr != -1);
	tmpfd = c_mkstemp(&tmpname: *c::char);
	assert(tmpfd != -1);
	const n = c_dup2(tmpfd, STDERR_FILENO);
	assert(n != -1);
};

@fini fn stderr_tmp() void = {
	if (tmpfd != -1) {
		c_close(tmpfd);
		c_unlink(&tmpname: *c::char);
	};
};
```

all low-level functions (libc wrappers or syscall wrappers) which may interact with stderr will silently adjust the fd from `STDERR_FILENO` to the dup'd stderr file descriptor if necessary. (i'll give an example later on, if you're confused by that.)

when an error occurs, the below function is called to obtain the error string:

```
fn get_errno() error = {
	c_perror(null);
	if (c_write(STDERR_FILENO, &0u8, 1) == -1) {
		return "Unknown error";
	};

	c_lseek(STDERR_FILENO, 0, SEEK_SET);
	defer c_lseek(STDERR_FILENO, 0, SEEK_SET);

	static let buf: [256]u8 = [0...];
	const n = c_read(STDERR_FILENO, &buf, len(buf));
	if (n == -1) {
		return "Unknown error";
	};

	const n = c::strnlen(&buf: *c::char, n: size);
	return *(&buf[..n]: *str);
};
```

when `perror` is called, it writes to the temporary file. (`STDERR_FILENO` needs to refer to the temp file from the beginning because `dup2` may overwrite errno, so it can't be used right before calling `perror`). the message is then read back and returned.

so cool, we have an error message string. now let's use that to obtain the actual errno value.

```
export type error = !str;
export type errno = !int;
```

to do that, i'll write another initialization function to construct a table of error messages:

```
let strerror_table: [256][256]u8 = [[0...]...];

@init fn strerror_table() void = {
	for (let i = 0; i < len(strerror_table): int; i += 1) {
		const msg = c_strerror(i);
		const n = c::strlen(msg);
		strerror_table[i][..n] = (msg: *[*]u8)[..n];
	};
};
```

the above function copies the strings returned by `strerror` into `strerror_table`. if you're familiar with posix libc, you may be confused why i'm not using `strerror_r` here, since that function directly writes error messages into a user-supplied buffer.

the reason is really annoying: there's two versions of `strerror_r`! there's a non-standard GNU version which returns an error message directly but may use its own immutable static storage, and there's a POSIX version which always writes to the user-supplied buffer and returns `int`.

i want the POSIX version of course, but on glibc, the symbol "`strerror_r`" refers to the GNU version. the POSIX version uses the symbol "`__xpg_strerror_r`". but other posix-compliant libcs (such as musl and all the BSD libcs) only provide the POSIX version, under the symbol "`strerror_r`". fitting with the theme of this blog post, glibc makes it impossible to portably call the function i want without relying on an internal symbol name, which i really don't want to do (since that makes the code libc-dependent).

but anyways, now that the `strerror_table` is constructed, let's update the `errno()` function to use it:

```
fn get_errno() error errno = {
	c_perror(null);
	if (c_write(STDERR_FILENO, &0u8, 1) == -1) {
		return "Unknown error" -1;
	};

	c_lseek(STDERR_FILENO, 0, SEEK_SET);
	defer c_lseek(STDERR_FILENO, 0, SEEK_SET);

	static let buf: [256]u8 = [0...];
	const n = c_read(STDERR_FILENO, &buf, len(buf));
	if (n == -1) {
		return "Unknown error" -1;
	};

	const n = c::strnlen(&buf: *c::char, n: size);
	return *(&buf[..n]: *str);
	if (n != 0) {
		buf[n - 1] = '\0'; // strip trailing newline
	};

	for (let i = 0z; i < len(strerror_table); i += 1) {
		if (c::strcmp(&buf: *c::char, &strerror_table[i]: *c::char) == 0) {
			return i: errno;
		};
	};
	return -1;
};
```

btw, here's an example of what a libc wrapper of `write` looks like:

```
export fn write(fd: int, buf: *opaque, count: size) (size | errno) = {
	const fd = if (fd == STDERR_FILENO) stderr else fd;
	const n = c_write(fd, buf, count);
	if (n == -1) {
		return get_errno();
	};
	return n: size;
};
```

## another fucked up hack to make it actually portable

the original thing that actually caused me to make this was that i wanted to see if i could make a generic "posix" target for hare. like, if you have a posix system which hare doesn't natively support, you could compile for the "posix" target and link to the system libc and things would just Work. the errno stuff was just a fun detour from that (the generic posix version would just return an error string, since the actual error numbers aren't portable). but in this context, there's one problem with the errno stuff. you can see it in this snippet from `get_errno()`:

```
c_lseek(STDERR_FILENO, 0, SEEK_SET);
defer c_lseek(STDERR_FILENO, 0, SEEK_SET);
```

i define `SEEK_SET` as a constant earlier on:

```
def SEEK_SET = 0;
```

this is usually true, but posix doesn't guarantee that `SEEK_SET` is 0! its value is completely implementation-defined. this is *yet another* place where it's not really possible to do stuff outside of c without relying on an "implementation detail".

(in practice i can't imagine any reason `SEEK_SET` *wouldn't* be 0, but unnecessary fucked up hacks are fun so for now let's just pretend that any of this actually matters)

it's actually possible to work around this! `lseek` returns the new offset after seeking. so we can call `lseek` with an arbitrary whence value, and use the return value to determine whether it did what we wanted. if not, just repeat with different whence values until it works!

```
let i = 0;
for (i <= 2; i += 1) {
	if (c_lseek(STDERR_FILENO, 0, i) == 0) {
		break;
	};
};
assert(i <= 2);
defer c_lseek(STDERR_FILENO, 0, i);
```

i just loop from 0 to 2, because i figure that it'll probably always be one of those values. but there's no reason higher values couldn't also be checked.

## conclusion

```
export fn main() void = {
	// demonstration: pass in -1 as the fd to cause EBADF
	match (write(-1, &0u8, 1)) {
	case size => abort("expected error");
	case let err: errno =>
		assert(err: int == sys::EBADF);
	};
};
```

i just think it's kinda cool that this is possible
