golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
124.07k stars 17.68k forks source link

Proposal: Basic C API for the runtime #17048

Open DemiMarie opened 8 years ago

DemiMarie commented 8 years ago

Purpose

This is a proposal for a basic C API for the runtime. It allows for non-Go main programs and for using specially written Go libraries from any language that can call C functions, with a few additional features beyond what are currently available by buildmode=c-shared. This will be a new buildmode.

Background

Go can call almost any C function via CGo. However, the ability to have a non-Go main program is much more limited. Go does have buildmode=c-shared, but it is limited by #11100 and also does not allow for useful features like passing GOMAXPROCS other than as an environment variable.

Goals

The entire API is provided in a single C header file, goapi.h.

Unless otherwise specified, any function can be called from multiple threads simultaneously. However, a single pointer in the API cannot be passed to API functions in 2 or more threads simultaneously, unless both functions take pointers to const data.

The API itself

int go_runtime_init(char * const* errormsg, ssize_t *already_initialized, const char * const *options, const void * const* arguments);

Initializes the Go runtime. Must be called before calling any other Go function. Thread-safe and may be called multiple times.

options is a pointer to an array of NUL-terminated strings. Each string corresponds to a matching void* in arguments that corresponds to a matching pointer, which points to a flag that can be used to configure the runtime. Currently, the only string in options that is meaningful is GOMAXPROCS, which replaces the GOMAXPROCS environment variable: the corresponding void* must point to a size_t*. All other values are reserved and must not be used.

If the defaults are OK, both arguments and options may be set to NULL. They are ignored if the runtime is already initialized.

Returns 0 on success, a negative number on failure.

int go_runtime_shutdown(void);

This function shuts down the runtime. It must be called once for each call to go_runtime_init. Only the last such call has any effect.

Once the number of calls to go_runtime_shutdown equals the number of calls to go_runtime_init, this function terminates all active goroutines and resets any signal handlers.

The following sentence may not be initially implemented: Afterwards, it is safe to dlclose the shared library containing the runtime, or to restart the runtime with go_runtime_init.

typedef struct Go_Handle *Go_Handle;

A handle to an arbitrary Go object. Analogous to a Haskell stable pointer. It stays valid even across garbage collections.

A Go_Handle is not guaranteed to point to valid memory. Dereferencing it invokes undefined behavior.

void go_runtime_delete_handle(Go_Handle handle_to_be_freed);

Deletes the handle passed as argument, rendering it invalid and freeing all underlying resources. After this function is called, the object the handle pointed to may be garbage collected if there are no more references to it.

Go_Handle go_runtime_duplicate_handle(Go_Handle handle_to_be_duplicated);

Duplicates the handle passed as argument. The returned handle points to the same object as the original handle, but must be deallocated separately.

uint8_t go_runtime_are_identical_handles(Go_Handle handle1, Go_Handle handle2);

Tests if the 2 handles passed are identical. Returns true if they point to the same object in memory. Otherwise, returns false.

The Go side

These are additional functions exposed from the runtime package.

type CHandle

The type of C handles to Go data. When passed via CGo, becomes a Go_Handle on the C side.

May be accessed by multiple goroutines simultaneously.

func NewHandle(object interface {}) CHandle

Creates a handle to a Go object that can safely be passed to C.

func DuplicateHandle(handle CHandle) CHandle

Duplicates the handle.

func TestIfIdenticalHandles(handle1 CHandle, handle2 CHandle) bool

Tests if the 2 handles passed point to the same object; that is, if modifications of the object pointed to by one will affect the object pointed to by the other.

func DeallocateHandle(handle CHandle)

Deallocates the handle, rendering it invalid.

func DereferenceHandle(handle CHandle) interface {}

Dereferences the handle, returning the contained object. If the handle is invalid, invokes undefined behavior.

minux commented 8 years ago

Note that having such an API won't automatically solve #11100, and it's stil very hard to implement go_runtime_shutdown as proposed.

Besides, c-shared already allows writing programs where the main is not written in Go. What's the real benefit of having such a C API as compared to the current c-shared support?

DemiMarie commented 8 years ago

There are two advantages.

The less important one is that go_runtime_shutdown is called explicitly, rather than implicitly on dlclose. This might be helpful.

The bigger one is the handle feature. This makes the task of turning arbitrary Go data into a void* used by C libraries much easier.

crawshaw commented 8 years ago

If you put aside #11100 for the moment (making go_runtime_shutdown work would also make -buildmode=c-shared work, it's the same problem), I believe you can build this interface today on top of c-shared.

You can declare CHandle to be an int, allocate them by incrementing a global counter, and keep a map[interface{}]CHandle and map[CHandle]interface{} in a management package. This is roughly how the inside of the gobind tool in the mobile repository works.

minux commented 8 years ago

I actually don't quite understand why is the CHandle stuff is useful. You're basically building a layer like JNI where C can't access anything directly and every access must go through function calls.

It's even worse than what could be achieved with today's c-shared mode; at least non-pointer data can be shared between Go and C freely. And Go can access memory allocated by C freely too.

ianlancetaylor commented 8 years ago

I believe the handle code can be written using an ordinary Go package.

You can write a non-Go program that calls Go code today using either buildmode=c-shared or buildmode=c-archive.

I don't understand how to implement go_runtime_shutdown. That's the hard part.

DemiMarie commented 8 years ago

@minux This is not intended to be like JNI at all. There is no analog of the JNI functions for accessing handles. In fact, the direct inspiration is Haskell's excellent Foreign Function Interface, which includes stable pointers (which refer to an arbitrary Haskell object, and which can then be passed back into Haskell). The purpose of a handle is not so that C code can manipulate Go objects. What it is is a way for Go code to wrap a reference to some Go value, such that a later callback from C into Go can use that value.

This can be implemented in the runtime more efficiently than in pure Go. For one, using a deleted handle is use-after free.

minux commented 8 years ago

I don't think it requires any special facilities from the runtime to be performant.

RLH commented 8 years ago

W.R.T. the level of indirection / pointer handle implementation the next step seems is a pure Go library implementation. This could then be used to generate real world use cases that in turn can identify if there are indeed performance bottlenecks the runtime/compiler need to look at. Expanding the runtime's API surface needs to be motivated by real world use cases that aren't apparent from this discussion.

On Tue, Oct 11, 2016 at 4:12 AM, Minux Ma notifications@github.com wrote:

I don't think it requires any special facilities from the runtime to be performant.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/17048#issuecomment-252842455, or mute the thread https://github.com/notifications/unsubscribe-auth/AA7Wn0IMFf9H_xZvFxBExEj4xapV1LJwks5qy0T2gaJpZM4J5mFi .

rsc commented 8 years ago

Like @ianlancetaylor said, the key insight for cgo was that people writing these kinds of bindings want to write Go code, not C code, so we made it so that wrappers are written in Go. We really don't want to write wrappers in C. C code can call custom cgo-exported Go code, of course.

Is there some specific functionality that this approach (write Go code called from C) doesn't work for?

DemiMarie commented 8 years ago

@rsc You are correct that writing stubs in C would not be a good thing. That is why .NET's PInvoke is much better than the JVM's JNI – marshaling is done in the higher level language, where it belongs.

I can think of two cases for this API. Neither involves writing stubs in C:

Since the latter use case is much more important to me, I am willing to restrict my proposal to it.

minux commented 8 years ago

It seems unlikely that you can unload a loaded Go plugin from a process. Even it's possible, the Go runtime will make sure no Go pointer is leaked when it's unloaded. I'm not sure what that has to do with this proposal. (because there is no way to forcibly stop a goroutine from outside, the host application can't simply ask the Go plugin to unload itself through runtime: the Go plugin must have designed with such a feature.

ianlancetaylor commented 8 years ago

@DemiMarie I have no objection in principle to support for shutting down the Go runtime, but I have no idea how to implement it. As @minux says, the Go runtime doesn't even have a way to shut down a stopped goroutine. I think that moving forward on this proposal would require a design that would make that possible. The design would have to not require major changes to the current runtime, because we do not want to rewrite everything to support a minor use case. Unless and until such a design is made, I would have to recommend that we decline this proposal.

rsc commented 8 years ago

@DemiMarie Let's assume the "handle" half of the suggestion is off the table, since, as you agree, it can be done on the Go side.

Then what's left is the go_runtime_shutdown. I think it would be plausible - provided it is not an invasive change - to add an exported C function like "bool go_runtime_shutdown(void)" that simply returns false (and does not do any shutdown) if there are any goroutines running. Then it's up to the author of the plugin to arrange that the goroutines are all gone before calling shutdown. If not, the shutdown doesn't shut anything down. As Minux and Ian said, this is the best one could really hope for.

I don't understand the need for go_runtime_init. The runtime already initializes itself, as far as I understand. It especially doesn't make sense to me to call it and not know whether the runtime is initialized or not. Again, the caller really needs to understand the state for any of this to work. It can't be stabbing in the dark.

minux commented 8 years ago

Also, if the use case is Go plugin into host applications, usually, the plugin interface of the host application should already provide a method to indicate to the plugin that it should shutdown. As you said, the host application might not be open source, so even if we have the C API to shutdown a Go plugin, it won't be called by the host application directly, otherwise, the host application must have designed for Go plugins.

Therefore, the plausible (direct) caller for the proposed shutdown function will be the Go plugin itself. And in that case, why do we need a C interface for that?

a runtime/plugin.Shutdown from the Go side should be enough (provided the plugin cleans up all the remaining goroutines first.) And even if a C callable function is needed, it's trivial to export that using regular cgo export mechanism.

The design of go_runtime_init and go_runtime_shutdown seems to allow recursive init calls, why is that necessary? The Go plugin will initialize itself when loaded, and all exported cgo calls will automatically wait until the runtime initialization has finished.

DemiMarie commented 8 years ago

@minux The problem with a Go side shutdown function is that you can't run any (user) Go code after the runtime has shutdown – the runtime Go needs isn't there anymore. It would work only if the function never returned.

Expecting the user code to shut down all goroutines is okay.

minux commented 8 years ago

I don't understand. The hypothetical runtime.Shutdown() will be similar to os.Exit(), except perhaps the code needs to be sure no other goroutine exists.

rsc commented 8 years ago

I wrote:

Then what's left is the go_runtime_shutdown. I think it would be plausible - provided it is not an invasive change - to add an exported C function like "bool go_runtime_shutdown(void)" that simply returns false (and does not do any shutdown) if there are any goroutines running. Then it's up to the author of the plugin to arrange that the goroutines are all gone before calling shutdown. If not, the shutdown doesn't shut anything down. As Minux and Ian said, this is the best one could really hope for.

I think this proposal is essentially on hold until someone does the work to find out whether this is an invasive change or not. It may be necessary to have sysmon shut itself down, but hopefully there isn't more to do.