-
Notifications
You must be signed in to change notification settings - Fork 570
Go values as objects and properties #704
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
Comments
If you take a look at the wiki tab under "Javascript Tips and Gotchas" you will see an example of putting a *js.Object field into a struct. This allows your go struct to interact with an underlying javascript object. |
I'm don't understand how that is related to storing go values as JavaScript objects and properties. Can you elaborate? |
@MaVo159, I think @4ydx misunderstood your question (or I did! :). You're talking about putting a Go object (such as a naked struct ( Yeah, I don't know how to do that either. I agree that you could get a *js.Object pointer to the Go object via Like so: https://gopherjs.github.io/playground/#/3xXHeQqtyZ But that still gives you a copy and not the original object back again. I've tested this exactly as much as it took to write the code in the playground, so YMMV, of course. |
@theclapp understood the question correctly. Thanks for the example. I'll take a look at it. Just to clarify why this is useful: I often need to attach tie data to dom elements so I can use it when processing events or methods called from JS. This is also the last missing piece for writing nice js libraries in go (I am currently working on some custom-elements for example). |
Storing a struct with a pointer to the struct you actually want seems to work. type S struct{ i int }
type PS struct{ ps *S }
s := S{i: 5}
ps := PS{ps: &s}
println(fmt.Sprintf("s: %#v, ps: %#v, *ps.ps: %#v", s, ps, *ps.ps))
o := js.Global.Get("Object").New()
o.Set("go_slot", js.InternalObject(ps))
newPS := PS{}
js.InternalObject(newPS).Set("ps", o.Get("go_slot").Get("ps"))
println(fmt.Sprintf("o: %#v, s: %#v, s.i: %v, newPS: %#v, newPS.ps: %#v, *newPS.ps: %#v",
o, s, s.i, newPS, newPS.ps, *(newPS.ps)))
println("set newPS.ps.i = 6")
newPS.ps.i = 6
println(fmt.Sprintf("o: %#v, s: %#v, s.i: %v, newPS: %#v, newPS.ps: %#v, *newPS.ps: %#v",
o, s, s.i, newPS, newPS.ps, *(newPS.ps))) => s.i == 6 |
This might be a little clearer, and a little more generic, to boot: type S struct{ i int }
type Wrapper struct{ p interface{} }
s := S{i: 5}
w := Wrapper{p: &s}
o := js.Global.Get("Object").New()
o.Set("go_slot", js.InternalObject(w))
var w2 Wrapper
js.InternalObject(w2).Set("p", o.Get("go_slot").Get("p"))
ptrS := w2.p.(*S)
println("w2.p.(*S).i:", w2.p.(*S).i, "s.i:", s.i, "ptrS.i:", ptrS.i)
// => w2.p.(*S).i: 5 s.i: 5 ptrS.i: 5
ptrS.i = 6
println("w2.p.(*S).i:", w2.p.(*S).i, "s.i:", s.i, "ptrS.i:", ptrS.i)
// => w2.p.(*S).i: 6 s.i: 6 ptrS.i: 6 |
No idea what is better or considered the official way of doing this.
|
Or just forgo the confusing bits and work with a struct. The former requires that you use *js.Object pointers while the code below requires that you manually set values when pulling data out of "go_slot".
|
@4ydx But, your first solution stores a js-special object, not a regular Go object, and your second solution doesn't get you a Go object back again. Am I missing something? So far I prefer my solution at #704 (comment). |
I'm just catching up so might have missed some intervening discussion on Slack/other. Using the combination of package main
import (
"unsafe"
"github.com/gopherjs/gopherjs/js"
)
type S struct {
Name string
}
func main() {
// Whatever the value is....
v := S{Name: "Rob Pike"}
// ...use js.InternalObject() on a _pointer_ to that value.
//
// Here we set a global variable in Javascript world to
// the result.
js.Global.Set("banana", js.InternalObject(&v))
// ....
// To then use that value again, get the *js.Object value...
vjo := js.Global.Get("banana")
// ... then use .Unsafe() + unsafe.Pointer
vp := (*S)(unsafe.Pointer(vjo.Unsafe()))
// Verify that we have exactly the same object
println(vp == &v, *vp == v)
} If we also look at the transpiled code we can see that's pretty much as expected: main = function() {
var v, vjo, vp;
v = new S.ptr("Rob Pike");
$global.banana = v;
vjo = $global.banana;
vp = ($pointerOfStructConversion((vjo), ptrType));
console.log(vp === v, $equal(vp, v, S));
}; (the slightly incongruous thing here being that struct values are also pointers in Javascript world; the difference is handled in the compiler) |
Thanks @myitcv, that's exactly what I was looking for. I think it would make sense to expose this as Maybe GetGoValue should panic if the requested field is not a go value. Or maybe the js.Object.Interface() should just be taught to recognize go values, which currently results in a crash (see https://gopherjs.github.io/playground/#/xaP0qP2yKZ for a modified version of @myitcv's example using |
Nice work @myitcv. I knew I wanted a C-style "just do it" cast, I just didn't know how to achieve it. I've never(?) used unsafe and forgot about it. |
@theclapp 👍 - @MaVo159
Unfortunately not; because to do so would mean the value gets Hence we need
... which is why I'm not in favour of making this sort of thing any more friendly 😄. So I wouldn't be in favour of this API change. For the sake of two or three lines I think the |
I fail to see the problem. I'm suggesting that the Returning the |
Ah, I see what you're saying. I don't think this is feasible. Reason being, not all Go values carry type information around with them. Hence You could instead "set" the js.Global.Set("banana", js.InternalObject(interface{}(&v))) but then when you get the value back out you'd still need to type-assert to do anything useful. So I don't think there's anything to be gained over the approach laid out in #704 (comment) Does the approach in #704 (comment) miss something you're after? Am I missing something obvious? |
I see. That makes sense.
A type assertion from interface{} will either panic or be safe, with unsafe there won't even be runtime type checking.
So we are back basically back to implementing the The idea to use Get/Set methods was probably misguided though. Maybe something like this:
(needs a better name that doesn't sound like MakeWrapper) Alternatively, maybe MakeWrapper can be modified a tiny bit to store type info, then we just need an Unwrap method. I would expect MakeWrapper to have unfortunate performance characteristics though. |
@FlorianUekermann I'm somewhat coming round to your suggestion that this should be exposed more formally. I recently distilled out the relevant functions in That said, given this is the first thread on the topic (that I'm aware of) there's perhaps a question of how prevalent such a requirement is? |
Looks good. That's pretty much identical to what I use.
I've seen discussions about this here and there. I just never saw it solved without resorting to a global map. |
Hello folks, I too have wrestling with this problem a lot while working on a state management binding for gopherjs. I believe this is a shortcoming of gopherjs which should be supported in the future. For now, I created the package |
Thanks @norunners - good to see a release of vert!
This particular discussion is specific to the case where the values do originate in the Go world, so I think vert looks to address a different problem. I'll also point out #790 in the more general context of values moving between the two worlds. @FlorianUekermann - I think I'd be tempted to agree with your conclusion that there is some merit in making this part of the Given // Cloak encapsulates a Go value within a JavaScript object. None of the fields
// or methods of the value will be exposed; it is therefore not intended that this
// *Object be used by Javascript code. Instead this function exists as a convenience
// mechanism for carrying state from Go to JavaScript and back again.
func Cloak(i interface{}) *Object {
// ..
}
// Uncloak is the inverse of Cloak.
func Uncloak(*Object) interface{} {
// ..
} |
Thanks @myitcv for explaining this particular problem more clearly to me. I am curious to what benefit is it to let the object be unusable in the js world? Package |
@norunners the concrete use case I have is my use of state in |
Both cases we have described are different and ideally, each should have some form of support. The cloak/uncloak method has the following limitations. 1) The state must always be cloaked before entering the JS layer. This is an extra inconvenient step as gopherjs automatically converts Go values to JS objects by nature. I ask, why not convert back from JS objects to Go values for non-primitive types if possible? 2) The cloaked state may not be used by the JS layer at all. This is a dealbreaker for some state manager behavior, e.g. getters and watchers. After encountering those issues, I was convinced to only focus on converting JS objects to Go values of any type, e.g. structs. After some refinement, package vert was developed. Beneficialy, this cleaned up all my usages of converting before the JS layer, resulting in only needing to convert unidirectional instead of bidirectional. Lastly, I recognized a common theme for JS libraries to prefer callback functions. This was problematic since we want Go function with type safe parameters. After the core convert logic was in place, I extended package vert to accommodate JS callbacks by allowing type safe Go functions to be called with JS objects. I’ll conclude this rabbit hole with a trivial hello world example. func main() {
object := js.Global.Get("Object").New("Hello World!")
function := vert.New(print)
function.Call(object)
}
func print(message string) {
fmt.Println(message)
} Note, function |
Because, in my case, it's an unnecessary overhead first converting from Go -> Javascript, and then Javascript -> Go, particularly when you a) lose type information and b) increase the CPU and memory cost, c) lose pointers... the list goes on. Best to just keep the Go values as Go values and "cloak" them.
Totally agree, it doesn't suit all cases. Hence why I pointed out #790 in #704 (comment).
One thing I'm not totally clear on is where the automatic conversions applied by the
Is this therefore equivalent to what you want? package main
import (
"fmt"
"github.com/gopherjs/gopherjs/js"
)
func main() {
object := js.Global.Get("Object").New("Hello World!")
print(object.String())
}
func print(message string) {
fmt.Println(message)
} Playground link: https://gopherjs.github.io/playground/#/SquXDT1mHT
Is this what you are referring to with getting package main
import (
"fmt"
"github.com/gopherjs/gopherjs/js"
)
func main() {
js.Global.Call("eval", `window.banana = {hello: "world", age: 1000}`)
v := js.Global.Get("banana").Interface()
fmt.Printf("v has type %T\n", v)
} Playground: https://gopherjs.github.io/playground/#/__MJlotUxS |
The cost this direction should be negligible and
I've demonstrated with package vert that the type information is not required in the JS layer and the JS object can still be converted to a Go value with the same type structure.
Yes a little bit, I haven't experienced any noticeable overhead with package [vert (https://github.com/norunners/vert) and it shouldn't block a
Again, I've demonstrated with package vert that it accommodate the same Go type that the user defines, with or without pointers.
This is a viable approach for simple use cases but I wanted to document where it falls short. So the community can be aware of the two solutions that solve two related but different problems.
The shortcoming of
print(object.String()) This too works for simple cases, but it's not generic at all since the client must be aware of the
Yes, this is possible but extremely inconvenient as it burdens the client to both key into the map and type assert each value needed instead of being able to provide a single struct that suits the JS object's structure. Below is your example from above, but with added type safety. type banana struct {
*js.Object
hello string `js:"hello"`
age int `js:"age"`
}
func main() {
js.Global.Call("eval", `window.banana = {hello: "world", age: 1000}`)
v := js.Global.Get("banana")
function := vert.New(print)
function.Call(v)
}
func print(v *banana) {
fmt.Printf("v has type %T\n", v)
} Output: |
For non- I'm unclear, do you see a particular problem with what I'm doing with the "cloak"/"uncloak" approach? |
Thank you for the clarification about conversion costs.
Your approach is awesome, correct and trivial to implement. It should be used when possible. If the user needs more flexibility in the JS layer (mentioned earlier) then package vert is the only effort I've seen to support these use cases. |
Storing Go addresses in the javascript world and later getting them back seems very useful. I'm surprised this thread hasn't garnered more comments. I had spent the morning trying to see how it was done by other GopherJS binding packages and then luckily came across this issue. I want to use the javascript WeakMap feature with Go addresses. Cloak() doesn't return the same *js.Object for each call with the same input, so it can't be used as the key for a javascript WeakMap. In fact, it seems to intentionally create a closure so that's more overhead than I would expect is needed - at least how I am seeing using Go as a better way to write javascript. This is how Cloak is transpiled for me:
Perhaps this is interesting because it may be able to return the address of the interface type itself, having captured it in a closure, rather than storing the two parts of the interface separately (which would also require a javascript object to be created). But since using the unsafe was now on the table, I wondered if it could be used more directly. I use it to convert a concrete pointer to a *js.Object, and back, and now the javascript WeakMap works with Go addresses. Thanks! |
FWIW, the Cloak function you're using isn't quite right. You're using func Cloak(v interface{}) *js.Object {
return js.InternalObject(&v)
}
func Uncloak(v *js.Object) interface{} {
return *(*interface{})(unsafe.Pointer(v.Unsafe()))
} This calls Try this: func Cloak(i interface{}) *js.Object {
return js.InternalObject(i)
}
func Uncloak(o *js.Object) interface{} {
return interface{}(unsafe.Pointer(o.Unsafe()))
} => Cloak = function(i) {
var i;
return i;
};
Uncloak = function(o$1) {
var o$1;
return ((o$1));
}; Example (using the GoConvey testing library): func TestCloak(t *testing.T) {
type testGoType struct {
Name string
}
Convey("Cloak/Uncloak", t, func() {
v := testGoType{Name: "My Name"}
cl := Cloak(&v)
v2 := Uncloak(cl).(*testGoType)
So(&v, ShouldEqual, v2)
cl2 := Cloak(&v)
So(cl == cl2, ShouldBeTrue)
})
}
% gopherjs test
..
2 total assertions
PASS
ok github.com/huckridgesw/hvue 0.443s |
Brilliant. Simply very, very timely help. Thank you. That is so much better. With that, empty interfaces can be passed to the javascript WeakSet too. |
That's probably as a result of @FrankReh having read #704 (comment), so mea culpa 😄 The one thing to note about the difference between the two is that with your approach, when you uncloak you need to know the exact type of the value that was cloaked. This might be ok in your situation but where interface values are concerned it can be inconvenient (because you need to know the pointer type to the original interface value as it was cloaked). That said, "my" approach does have the noted overhead (not sure how significant it ends up being). |
I don't follow. (Not saying you're wrong, just that I don't understand.) Both our functions return |
Hmm, I was just trying to pull together an example of what I meant, and I can't. Which makes me think I've actually got this wrong. Indeed per myitcv/react#153, I've sufficiently proved to myself that I'm wrong 👍 Thanks @theclapp! |
It's very interesting. The cloak and uncloak functions become no-ops, simply returning the address they were passed. They serve to make the gopherjs compiler happy - which is great because the gopherjs compiler is half the reason for doing things this way. And it still feels lucky, or the result of the natural progression of garbage collection design, that this can work - the assumptions the go compiler makes about the runtime gc (mostly that it is a tracing gc) are aligned with the modern browser javascript runtimes. I'm so used to reference counting in obj-c and swift or nothing in C - that it seems natural that a variable would need to be boxed. But here, there are not two runtimes trying to coexist. There's just the browser runtime with the go language assumptions being mapped into it, and some map directly. The address of the interface structure being passed to cloak, and coming back from uncloak, I also started to think of as opaque data to javascript, and for a moment, I was surprised the javascript garbage collector could "see" inside. And then I remembered, again, that when this is running, it is all js, and in fact other js code could be written to look into that data that I was considering opaque - and the runtime that the gopherjs team has built does exactly that already. It's all very cool and I'm still trying to get my head around how it works and what it is making possible. Weakmaps and proxies are now on the table for go that is designed for gopherjs. |
Yes, that's true (as @theclapp pointed out), but also note the call site to see what transformation happens there (in the compiler). So given: package main
import (
"fmt"
"unsafe"
"github.com/gopherjs/gopherjs/js"
)
func main() {
var a uint8 = 5
b := Cloak(a)
c := Uncloak(b)
fmt.Printf("%T %v\n", c, c)
}
func Cloak(i interface{}) *js.Object {
return js.InternalObject(i)
}
func Uncloak(o *js.Object) interface{} {
return interface{}(unsafe.Pointer(o.Unsafe()))
} the relevant part of the transpiled output are the two functions:
but also the call site for
|
Wrapping a uint8 to an interface is just done that way by the gopherjs compiler I think. The go compiler also has some work to do to convert a uint8 to an interface type - and perhaps if escape analysis says it is warranted, allocate memory for the interface object. If I'm cloaking an interface to begin with, then gopherjs adds no additional wrapping. (At least that's what I though I saw when I worked with it yesterday.) |
Yes, indeed, but that's because the representation of values of numeric Go types does not carry type information around with it. So I was just including it as effectively an edge case of where the call site does matter.
Yep, completely agree. |
There is no straightforward way of storing go values in a js object or property. I know this restriction is intentional, but it is a very inconvenient one at times. All workarounds I know of have severe problems (serialization overhead, global map[string]interface{} won't be garbage collected).
I suspect that there are proper workarounds for this problem using js.InternalObject, but it is unclear to me how to recover the go value from the resulting *js.Object and if I can use it with js.Object.Set/Get.
The text was updated successfully, but these errors were encountered: