ebitengine/purego

Provide access to errno

dominikh opened this issue · 10 comments

Operating System

  • Windows
  • macOS
  • Linux
  • FreeBSD
  • Android
  • iOS

What feature would you like to be added?

In cgo, C function calls can optionally return an additional value, the value of errno after returning from the function (in the form of an error). Purego should provide something similar.

Why is this needed?

Being able to read errno is important to get useful error information. errno is thread-local, which means we cannot read it ourselves without locking the goroutine to its thread. Furthermore, errno is allowed to be a macro, so there is no straightforward, non-cgo way of reading it.

Hey @dominikh,

you can use dlsym to get the address of errno. The uinx / syscall package also allows you to convert them into into string or whatever.

Is that what you are looking for?

package main

import (
	"fmt"
	"unsafe"

	"github.com/ebitengine/purego"
	"golang.org/x/sys/unix"
)

func main() {
	libc, err := purego.Dlopen("libc.so.6", purego.RTLD_LAZY)
	if err != nil {
		panic(err)
	}

	var fopen func(filename, mode string) unsafe.Pointer
	purego.RegisterLibFunc(&fopen, libc, "fopen")
	errno, err := purego.Dlsym(libc, "errno")

	file := fopen("this file does not exist!", "r")
	if file == nil {
		if err != nil {
			panic(err)
		}
		// using &errno prevents go vet yelling "possible misuse of unsafe.Pointer"
		fmt.Println((*(**unix.Errno)(unsafe.Pointer(&errno))).Error())
	}
}
[jupiterrider@pc42 demo]$ go run .
no such file or directory

What happens if between the call to fopen and the read of errno Go moves the goroutine to a different thread? At that point we'd be reading errno of the wrong thread.

I also don't believe that this is portable across different libc implementations. While this seems to work for glibc, it doesn't for musl (node-ffi/node-ffi#273). What about macOS or the BSDs?

I don't actually know if the portability issue is solvable without the use of C.

Regarding portability, here are two ways in which the Rust ecosystem is handling access to errno on Unix systems, both of which boil down to "hard-code per-platform options". It seems that on Linux, the various libcs agree to provide __errno_location. This was part of the LSB.

  1. https://github.com/rust-random/getrandom/blob/c5e2025d2cdb29355ac80557e67def1eb7ea477c/src/util_libc.rs#L14-L38
  2. https://github.com/rust-lang/rust/blob/master/library/std/src/sys/pal/unix/os.rs#L42-L76

What happens if between the call to fopen and the read of errno Go moves the goroutine to a different thread? At that point we'd be reading errno of the wrong thread.

Dlsym is the recommended way. Use runtime.LockOSThread to ensure the thread doesn't change. I don't believe musl is even supported. There were some messages on the discord about having issues. Dlsym should work on macOS

With the usage of the __errno_location() / __error() function, you can solve the threading issue without using runtime.LockOSThread. And yes, this cannot be avoided without writing platform specific code.

What is even the need of having this feature in purego?

With the usage of the __errno_location() / __error() function, you can solve the threading issue without using runtime.LockOSThread.

I don't see how __errno_location solves the threading issue. Thread-local storage will have the same virtual address on every thread. In glibc, this is the definition of the function:

int *
__errno_location (void)
{
  return &errno;
}

Even if that wasn't the case, and __errno_location returned unique locations per-thread, we'd still need to use LockOSThread to ensure we're on the same thread when we call __errno_location and when we call a function that sets errno.

What is even the need of having this feature in purego?

Are you asking why this has to be in purego itself, or why any purego user would need to access errno?

@dominikh
You are right. runtime.LockOSThread is required. I misunderstood the description of __errno_location. Sorry.

Are you asking why this has to be in purego itself, or why any purego user would need to access errno?

Why it has to be in purego itself.

purego is kind of the cgo-free Version of dlfcn.h.

Why it has to be in purego itself.

In terms of necessity, I was hoping that purego could avoid the cost of calling LockOSThread by reading and storing errno in the same cgocall as calling the C function. I don't know if that is feasible, however.

In terms of API design, it makes sense to me that purego would offer some help with getting errno, as that is the interface with which the majority of functions do error reporting. Otherwise, every user will have to rediscover on their own that every platform needs a different way of accessing errno, and be aware of the multithreading caveat. Or more likely they will find a way of accessing errno that works on their system (such as #244 (comment)), not be aware of the need for LockOSThread, and write code that doesn't work reliably.

Users of cgo, or C users of dlfcn.h, do not face this problem, as in both cases they get automatic access to errno. Cgo turns errno into an error return value, and C is C.

In terms of necessity, I was hoping that purego could avoid the cost of calling LockOSThread by reading and storing errno in the same cgocall as calling the C function. I don't know if that is feasible, however.

Is this cost actually significant in any profiles?

I'm not against this feature. purego.SyscallN definitely could return the errno without changing the api since it matches the syscall package which does return it. It currently just defaults to zero because it wasn't needed for ebitengine. If we were going to add it I'd like to see if we could also add it to RegisterFunc but at this moment I don't know a good api for that.

Is this cost actually significant in any profiles?

Looking into this more (golang/go#21827), most of the cost of LockOSThread is for locked goroutines that communicate with other goroutines (e.g. via channels.) The raw cost of calling LockOSThread is a handful of atomic reads and writes, so not that noteworthy in isolation.

I'd like to see if we could also add it to RegisterFunc but at this moment I don't know a good api for that.

You could allow Go function types with an error return value as their last one. When it is present, return the errno in it.