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

Pass serialized Go structs to TinyGo #523

Closed
knqyf263 opened this issue May 4, 2022 · 14 comments
Closed

Pass serialized Go structs to TinyGo #523

knqyf263 opened this issue May 4, 2022 · 14 comments

Comments

@knqyf263
Copy link
Contributor

knqyf263 commented May 4, 2022

First of all, thanks for the allocation examples.
https://github.com/tetratelabs/wazero/tree/main/examples/allocation

I assumed serialized Go struct can be passed to WASM as a byte array like the above examples and it can be decoded in WASM so that we can pass Go structs to WASM and vice versa. I started with JSON serialization as some JSON libraries seem to work with TinyGo. But fastjson doesn't support struct unmarshaling AFAIK and easyjson didn't work in my environment as below.

2022/05/04 08:06:32 wasm error: unreachable
wasm stack trace:
        .runtime.runtimePanic(i32,i32)
        .runtime.slicePanic()
        .(*github.com/mailru/easyjson/jlexer.Lexer).FetchToken(i32)
        .(*github.com/mailru/easyjson/jlexer.Lexer).IsNull(i32) i32
        .greet(i32,i32)

TinyGo code is like the following.

//export greet
func _greet(ptr, size uint32) {
	var b []byte
	s := (*reflect.SliceHeader)(unsafe.Pointer(&b))
	s.Len = uintptr(size)
	s.Data = uintptr(ptr)

	var t foo.TestStruct
	if err := easyjson.Unmarshal(b, &t); err != nil { // fail here
		log(err.Error())
	}
        ...
}

Do you have any recommendations for passing Go structs? It doesn't have to be JSON.

@codefromthecrypt
Copy link
Contributor

I'll re-title this as serialized struct as it is less mysterious than the other way to pass structs around (via reference). Intentionally not getting into the latter approach as there's still quite a lot of learning to do. Suffice to say if you are interested in reference (pointer) based approach @rag has some interesting work started here anuraaga/opentelemetry-collector-contrib#633

I'll reply back on the serialization approach in a bit!

@codefromthecrypt codefromthecrypt changed the title Pass Go struct to TinyGo Pass serialized Go structs to TinyGo May 4, 2022
@codefromthecrypt
Copy link
Contributor

ps you didn't ask, but this works for parsing.

I'm using a fork of @birros to play with this because this easygo has some dependencies needed. I'll update what if anything I figure out birros/wazero-demo@main...codefromthecrypt:easyjson-debugging

@codefromthecrypt
Copy link
Contributor

I think the problem was that we didn't set Cap on the underlying slice. When I did that, it worked. See also #524

@inkeliz
Copy link
Contributor

inkeliz commented May 4, 2022

I think the best way is using Google Flatbuffers or Cap'n'Proto. Flatbuffers is supported on TinyGo and Go, and have zero-copy decoding, so you don't pay the price for decoding or accessing single/random values. Also, Flatbuffers have 'Object-API' which makes easier to encode structs "into linear memory", so it's "similar" to json.Marshal(), for the host.

I don't think any "schema-less" encoding will be good. Schema is unavoidable for zero-copy serializers and fast encoders. I think it's the best and fastest way to share data, with minimal overhead. If performance is not a concern you can also try Protobuf, it works with TinyGo (with some hack): tinygo-org/tinygo#447 (comment).


If your struct doesn't contains slices/strings and only uses uint32 and int32 (...), you can convert it to slice and then transfer it (indirectly). However, keep in mind that TinyGo is 32bits and Wazero is, probrably, running on 64-bit. That is important, because []something (any slice) is 3 words: it's 12 bytes on 32-bit and 24 on 64-bit. So, it will not match. The same applies for int. The int64/float64 isn't safe either, because it changes the struct-padding.


I'm also creating one serializer for that purpose (currently, it supports Zig, Go and AssemblyScript) and it's faster and safer than Flatbuffers (it's ~80% faster than Flatbuffers on Wazero, compiled with TinyGo -gc conservative), I hope to share it until the end of the next week. I'm currently implementing support for Swift. I'm also testing, fuzzing and benchmarking everything using Wazero, which is very nice, one pure-go single test-bench is working against all languages. 🤯

@codefromthecrypt
Copy link
Contributor

@inkeliz thanks for sharing and very exciting! sounds like something I can play with while at GlueCon!

@codefromthecrypt
Copy link
Contributor

@knqyf263 ps regardless, let me know if declaring the slice specifying Cap worked for you.

You'll need a stable byte slice declaration regardless of tool I think. except if you end up using host pointers serialized as uint64 (less ideal) or externref (more safe as as you can't accidentally allow pointer arithmetic because it has no ops like that).

@knqyf263
Copy link
Contributor Author

knqyf263 commented May 6, 2022

@codefromthecrypt Thanks for updating. Yes, Cap works for me! Very cool! I'm also interested in the pointer-based approach. I'll look into the PR.

@inkeliz Thanks for suggesting the Flatbuffers approach. I actually referred to the issue in TinyGo before raising this issue and wondered if protobuf is the best. I'll try Flatbuffers. Also, I'm really looking forward to your serializer.

@knqyf263
Copy link
Contributor Author

knqyf263 commented May 6, 2022

ps you didn't ask, but this works for parsing.

@codefromthecrypt I also tried gjson as I saw the following comment, but it doesn't support struct marshaling. Please correct me if I'm wrong.
https://github.com/tetratelabs/proxy-wasm-go-sdk/blob/998ed3c844be4912df88f2ce0214016b1e9dd479/examples/json_validation/main.go#L62-L64

@mathetake
Copy link
Member

yeah afaik gson can only be used for accessing each field individually, not for serde entire structs tidwall/gjson#193

@knqyf263
Copy link
Contributor Author

knqyf263 commented May 8, 2022

Protocol Buffers doesn't seem to work with TinyGo as of today. I faced the same error.
tinygo-org/tinygo#2806

@knqyf263
Copy link
Contributor Author

knqyf263 commented May 8, 2022

Anyway, thank you all. I actually need a schema-less approach this time. I will go with easyjson for now. Let me close this issue.

@knqyf263 knqyf263 closed this as completed May 8, 2022
@codefromthecrypt
Copy link
Contributor

@knqyf263 ps @inkeliz made something you may enjoy looking at. https://github.com/inkeliz/karmem

@knqyf263
Copy link
Contributor Author

Cool! I'm wondering if it supports interface{} since we have it in our struct for extensibility. I'll look into it. Thanks!

@inkeliz
Copy link
Contributor

inkeliz commented May 12, 2022

Currently, interface{} is not supported, and it's not possible to handle it directly. However, Flatbuffers and Cap'n'Proto support union, which I didn't implement, but I hope to implement that. I have some ideas to implement union (interface{} on Golang). However, each method has unique (performance) problems. 😅

One reason why my serializer is faster than Flatbuffers because it doesn't generate a lot of garbage and re-uses the same existent struct, slices, etc. In my case, that is very important, and that seems to work well for TinyGo. The issue with interface{} is that it always escapes to the heap. For random access, it's not an issue, and I can use some hack, which creates one interface by using [2]uinptr and so on, with some map to store the "metadata-pointer". However, decoding it back to one struct is not efficient. It's harder to re-use the same struct for multiple reads, forcing the decoder to implement an internal "sync.Pool" or generate (more) garbage. In order words, the naive way of yourStruct.Field = viewer.Something() will put pressure on GC since reading each message will cause to invalidate everything, including all internal slices (and slice inside slices) of the given "Something()". 😬

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

4 participants