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

v2: Create with support for both GopherJS and GOOS=js GOARCH=wasm. #57

Closed
dmitshur opened this issue Jun 20, 2018 · 14 comments · Fixed by #69
Closed

v2: Create with support for both GopherJS and GOOS=js GOARCH=wasm. #57

dmitshur opened this issue Jun 20, 2018 · 14 comments · Fixed by #69

Comments

@dmitshur
Copy link
Collaborator

Go is getting Wasm support in 1.11: https://tip.golang.org/doc/go1.11#wasm.

A lot of frontend Go code uses this package, so supporting js/wasm would be very helpful.

@yml
Copy link
Contributor

yml commented Jul 4, 2018

I have attempted to do this in this branch
I would happily change anything if it can help to achieve the goal in this ticket.

@dmitshur
Copy link
Collaborator Author

dmitshur commented Jul 5, 2018

@yml Thanks for looking at this and sharing your work. I think that's a great step forward.

The approach I'd like us to use for dom is going to be slightly different than the one you've used, but it should quite easy to adapt your code.

My proposed approach is for us to have separate files, one with // +build js,!wasm build tag (for GopherJS) and another with // +build js,wasm build tag (for Wasm). The current dom.go will remain unmodified, and the new dom_wasm.go will be very similar to the modified dom.go file in your branch, except it can use syscall/js directly.

That way, the dom API (and implementation) will be unmodified for GopherJS users and won't break any code. For example, the following change is not compatible with existing code, so we can't proceed with making it:

@@ -131,74 +131,76 @@ type Event interface {
 	PreventDefault()
 	StopImmediatePropagation()
 	StopPropagation()
-	Underlying() *js.Object
+	Underlying() js.Value
 }

We'll need to maintain and keep in sync two files rather than one, but I think that's the only viable way to proceed. Luckily, this package is mostly complete and doesn't change much, so it's not a very high cost.

If that makes sense to you, and if you'd like, feel free to make a PR with that approach. Otherwise, I or someone else can get around to it later.

From the yml#1 description, you said you only tested that it compiles. One of the additional steps we'll need to do, before we can merge a PR into this package, is test that it functions as expected.

@yml
Copy link
Contributor

yml commented Jul 5, 2018

@shurcooL thank you very much for your feedbacks. PR #59 implements the changes you have described above recommended.

@dmitshur
Copy link
Collaborator Author

dmitshur commented Jul 6, 2018

@dominikh, in PR #59, you said:

Well, I'm definitely not a fan of the massive amount of code duplication. That is to say, I'd very much like to avoid it.

(I wanted to move the discussion here, so the PR discussion can stay focused on the approach described in the PR description.)

I would like to avoid it too, but out of all alternatives, it seems the least bad. I'm happy to hear any suggestions that are better. I'm aware of the following alternatives (that do not seem better):

  • Implementing js: Consider adding Wasm support. gopherjs/gopherjs#833. Not seeing movement there. If it happens in future, the duplication can be easily removed.
  • Changing the dom API. As is, it has methods that return *github.com/gopherjs/gopherjs/js.Object type, which is not compatible with Wasm. This would break existing applications.

Basically, with this solution, we are creating 2 separate dom packages at the same import path with the same public API. One is the current GopherJS implementation (unmodified), and the second is the very similar Wasm one. I think that's better than having 2 separate dom packages with the same public API at different import paths.

@dmitshur
Copy link
Collaborator Author

dmitshur commented Jul 8, 2018

I think I've found a potentially show-stopping problem with implementing this issue. This package relies very heavily on the js struct field tag feature of GopherJS to access properties of many objects. E.g.:

go-js-dom/dom.go

Lines 1802 to 1811 in 6da835b

type HTMLAnchorElement struct {
*BasicHTMLElement
*URLUtils
HrefLang string `js:"hreflang"`
Media string `js:"media"`
TabIndex int `js:"tabIndex"`
Target string `js:"target"`
Text string `js:"text"`
Type string `js:"type"`
}

Wasm has no support for this feature (the only way to access fields is via the Get method of js.Value), and as far as I know, it's not planned (please correct me if I'm wrong @neelance).

That means the current dom API is not going to be possible to implement for Wasm fully. I don't have ideas for how to deal with this yet.

@yml
Copy link
Contributor

yml commented Jul 9, 2018

I guess the alternative is to change these fields to be a method with the same name. These is yet another difference in the API with the gopherjs equivalent.

The question is how much of API differences can we tolerate under the same import path before feeling clunky.

@cryptix
Copy link

cryptix commented Jul 9, 2018

The question is how much of API differences can we tolerate under the same import path before feeling clunky.

I fear none. I assume @shurcooL was aiming for equal api so that it won’t be a breaking change, just an additional implementation.

@dominikh
Copy link
Owner

dominikh commented Jul 9, 2018

I fear none. I assume @shurcooL was aiming for equal api so that it won’t be a breaking change, just an additional implementation.

ACK. If the APIs are different, they should be different packages, i.e. different import paths.

@dmitshur
Copy link
Collaborator Author

Agreed.

@dominikh How do you feel about a dom v2 API at the import path honnef.co/go/js/dom/v2 (package name dom)?

The API would be the same as v1, except current js fields would be replaced by methods. The Underlying() *js.Object stuff will need to be discussed/figured out.

@dominikh
Copy link
Owner

Ugh. Fine.

@myitcv
Copy link

myitcv commented Jul 10, 2018

How do you feel about a dom v2 API at the import path honnef.co/go/js/dom/v2 (package name dom)?

Just to confirm, you're effectively referring to a vgo v2 of honnef.co/go/js/dom?

@dmitshur
Copy link
Collaborator Author

dmitshur commented Jul 10, 2018

The import path was conceived with Go modules (vgo) and semantic import versioning in mind, but for now, I pictured it as just a normal directory named v2 in the same repository on master branch, containing a dom package.

If I understand correctly, it should be compatible with module-less Go 1.10 and intersect well with Go modules after that's released (1.11 and onwards).

Does that sound like a reasonable approach @myitcv, or is there something you'd suggest changing? I'm not yet very familiar with Go modules and how one is supposed to create packages for it.

@dmitshur dmitshur changed the title Support GOOS=js GOARCH=wasm. v2: Create with support for both GopherJS and GOOS=js GOARCH=wasm. Jul 10, 2018
@myitcv
Copy link

myitcv commented Aug 12, 2018

Given the /v2 API is only going to be relevant for 1.11 and onwards, is there a reason to go for a subdirectory at all? You could do all that work on a separate branch and simply tag releases for v2.x.x, which module-aware Go 1.11 will pick up if the /v2 import path is used.

@dmitshur
Copy link
Collaborator Author

dmitshur commented May 18, 2019

I have an update on this.

@hajimehoshi has added support for the syscall/js API to GopherJS 1.12-2 in PR gopherjs/gopherjs#908. That change makes it possible for a syscall/js-based implementation of the dom package to compile and work successfully in Go WebAssembly and GopherJS. That in turn means we no longer need to have two separate .go files, one with js,!wasm and another with js,wasm build constraints.

When we last attempted this in PR #59, we discovered a blocking problem that a significant breaking API change will be necessary, and so we'll need to use a different import path per the semantic import versioning convention, to avoid breaking existing programs importing the current v1 API.

I've taken the previous work by @yml in PR #59 and implemented the following changes on top of it:

  1. merged the GopherJS and WebAssembly .go files into one (i.e., kept the syscall/js implementation only)
  2. moved the code into a v2 directory so that its import path will be honnef.co/go/js/dom/v2
  • I chose to go with the major subdirectory approach, because it's compatible with both GOPATH mode and module mode (while major branch approach works only with module mode), and we want to continue to support GOPATH mode for now
  1. converted all struct fields with js:"foo" tags to equivalent methods; a change that was necessary because the syscall/js API does not support those js:"foo" tags
  2. made js.Value embedded in BasicNode (instead of being not embedded and called Object)
  • I was able to work around the name collision by using the Underlying() method in a few places where the underlying Value was needed but was shadowed by a Value() string method
  1. fixed the implementation of methods that work with callbacks to use the js.Func types rather than func(*js.Object) as before

In my testing, the new v2 API is fully functional with Go WebAssembly and GopherJS. I have not found any major problems. I'll send a PR for it now (crediting @yml as a co-author).

dmitshur added a commit that referenced this issue May 18, 2019
This change implements the starting point of a new v2 major version
of the dom package.

It is now implemented on top of the syscall/js API, which is supported
by Go WebAssembly and GopherJS (as of version 1.12-2). The design goal
has been to stay as close as possible to v1 API to make the transition
easier to make. The only changes that were done were out of neccessity:

- All struct fields with `js:"foo"` tags have been replaced with
  equivalent methods
- Underlying() returns js.Value instead of *js.Object
- AddEventListener() returns js.Func instead of func(*js.Object)

There is a remaining TODO in window.RequestAnimationFrame method
to make it so that cancelling the callback via CancelAnimationFrame
makes it so the js.Func wrapper's Release method is called. Arranging
this is difficult (it requires either having an internal map and sync
primitive for tracking js.Func by IDs, or changing the return value of
the RequestAnimationFrame method from int to something where the
js.Func and its Release method can be accessed easily.

The current implementation is functional but may end up using more
memory in some situations. It's not blocking for the initial alpha
release, so it remains a TODO for now.

This implementation was based on work done in by @yml in
Pull Request #59.

Fixes #57.

Co-authored-by: Yann Malet <yann.malet@gmail.com>
dmitshur added a commit that referenced this issue May 26, 2019
This change implements the starting point of a new v2 major version
of the dom package.

It is now implemented on top of the syscall/js API, which is supported
by Go WebAssembly and GopherJS (as of version 1.12-2). The design goal
has been to stay as close as possible to v1 API to make the transition
easier to make. The only changes that were done were out of necessity:

- All struct fields with `js:"foo"` tags have been replaced with
  equivalent methods
- Underlying() returns js.Value instead of *js.Object
- AddEventListener() returns js.Func instead of func(*js.Object)
- GetBoundingClientRect() returns *Rect instead of ClientRect

Update GetBoundingClientRect to return DOMRect instead of the legacy
ClientRect. A breaking API change was needed to make it return a pointer
rather than a value, so it was a good opportunity to also update the
type at the same time.

There is a remaining TODO in window.RequestAnimationFrame method
to make it so that canceling the callback via CancelAnimationFrame
makes it so the js.Func wrapper's Release method is called. Arranging
this is difficult (it requires either having an internal map and sync
primitive for tracking js.Func by IDs, or changing the return value of
the RequestAnimationFrame method from int to something where the
js.Func and its Release method can be accessed easily.

The current implementation is functional but may end up using more
memory in some situations. It's not blocking for the initial alpha
release, so it remains a TODO for now.

Document API stability; v2 is currently in alpha and more API changes
may be done before it exits alpha state.

This implementation was based on work done by @yml in
Pull Request #59.

Fixes #57.

Co-authored-by: Yann Malet <yann.malet@gmail.com>
Co-authored-by: Dominik Honnef <dominik@honnef.co>
GitHub-Pull-Request: #69
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants