Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide access to errno #244

Open
3 of 6 tasks
dominikh opened this issue May 11, 2024 · 10 comments
Open
3 of 6 tasks

Provide access to errno #244

dominikh opened this issue May 11, 2024 · 10 comments
Labels
enhancement New feature or request

Comments

@dominikh
Copy link

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.

@JupiterRider
Copy link
Contributor

JupiterRider commented May 12, 2024

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

@dominikh
Copy link
Author

dominikh commented May 12, 2024

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.

@dominikh
Copy link
Author

dominikh commented May 12, 2024

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

@TotallyGamerJet
Copy link
Collaborator

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

@JupiterRider
Copy link
Contributor

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?

@dominikh
Copy link
Author

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?

@JupiterRider
Copy link
Contributor

JupiterRider commented May 12, 2024

@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.

@dominikh
Copy link
Author

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.

@TotallyGamerJet TotallyGamerJet added the enhancement New feature or request label May 13, 2024
@TotallyGamerJet
Copy link
Collaborator

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.

@dominikh
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants