Skip to content

Support go:linkname compiler directive #1000

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

Closed
nevkontakte opened this issue Mar 18, 2021 · 5 comments
Closed

Support go:linkname compiler directive #1000

nevkontakte opened this issue Mar 18, 2021 · 5 comments

Comments

@nevkontakte
Copy link
Member

Motivation: this directive is actively used in low-level standard library code, in particular with the runtime package. Not having it available in GopherJS forces us to introduce inelegant hacks in standard library overlays. This functionality becomes particularly important when a function is exposed into several packages, making then share state (e.g. runtime.fastrand()).

Specification from https://pkg.go.dev/cmd/compile:

//go:linkname localname [importpath.name]

This special directive does not apply to the Go code that follows it. Instead, the //go:linkname directive instructs the compiler to use “importpath.name” as the object file symbol name for the variable or function declared as “localname” in the source code. If the “importpath.name” argument is omitted, the directive uses the symbol's default object file symbol name and only has the effect of making the symbol accessible to other packages. Because this directive can subvert the type system and package modularity, it is only enabled in files that have imported "unsafe".

@nevkontakte
Copy link
Member Author

Having thought about this feature for a while I think there are two main difficulties with implementing it in GopherJS context:

  1. The features is designed around a traditional binary linking approach when exported symbols get assigned globally unique names in the import/path.SymbolName format. GopherJS doesn't quite do that, instead it creates multiple different bindings (variables) referencing the same symbol for use in different context (e.g. package-locals vs cross-package). The reason why it matters is that to change the symbol you need to assign it to all of them.
  2. Interaction with GopherJS natives overlays. Technically a //go:linkname directive can come in any place in a package source code, how should that interact with symbols that GopherJS overrode with its own implementation? Do we need to add another directive (e.g. //gopherjs:nolinkname) that cancels out a particular linkname directive in the upstream code?

In terms of behavior, there seems to be two main ways this directive is used.

First it is to "import" a function implementation from another package even if it is unexported. This has an effect similar to force-exporting a symbol. Note that this can't be substituted with an import in a general case because it may create a circular dependency between packages. For example, somepackage here gains access to otherpackage internals, with no cooperation on the otherpackage side.

package somepackage

//go:linkname doSomething otherpacakge.doSomethingInternal
func doSomething() error
package otherpackage

func doSomethingInternal() error {
  // The actual implementation goes here.
  return nil
}

The second use case is to "insert" a function implementation from one package into another, again potentially violating exporting rules:

package somepackage

// This function is actually implemented elsewhere.
func doSomethingInternal() error
package otherpackage

//go:linkname doSomething somepackage.doSomethingInternal
func doSomething() error {
  // The actual implementation goes here
  return nil
}

Also it's worth noting that the feature can be used for variables as well, but I haven't actually encountered that, so we could get away with a partial implementation here.

@nevkontakte
Copy link
Member Author

A few more notes after playing with the Go's implementation of go:linkname:

  • The use case of "inserting" an implementation into another package (the second example in the comment above) appears to be somewhat a special case with the runtime package exporting its functions into various other packages. For example, fastrand() is exported into sync, net and os.
  • Inserting a function from package A to package B causes a segfault when B depends on A, and works if A depends on B. This matches the standard library use case (os de-facto depends on runtime).
  • In a general case the compiler will throw a "missing function body" error when you are "inserting" an implementation from another package, since it only considers linkname directives in the current package. For several standard library packages there is a hard-coded bypass of this check. This is a strong signal that the preferred use case for this is to "import" a function, which makes sense from readability perspective.

@nevkontakte
Copy link
Member Author

Side note, the discussion of this feature has gotten split between this issue and #989 (comment), so I'm explicitly cross-referencing it here.

@nevkontakte
Copy link
Member Author

After doing more research, I think here are the requirements for the go:linkname implementation in the GopherJS context:

  • Support the "importing" variant of the directive, which must work regardless of the package relation in the import graph. The "inserting" variant is not needed, but if it turns out easy to implement we might as well have it.
  • Only support linknaming functions. even though the upstream implementation claims to support it for variables and other symbols I haven't seen a good use case for that, so we can spare some complexity.
  • Dead-code elimination must preserve functions which are linknamed from another package without a need for additional tricks.
  • By the time any of the package's initialization code runs (in variable assignments or init() function) all linknamed functions must be configured correctly.
  • Any unsupported uses of the directive should trigger an explicit error at compile time.

Here's a basic test case that the implementation must be able to pass. nevkontakte@fc975de

@nevkontakte
Copy link
Member Author

This is now implemented and documented in https://github.com/gopherjs/gopherjs/blob/master/doc/pargma.md#golinkname

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

No branches or pull requests

1 participant