Skip to content

Commit d51afca

Browse files
authored
Merge pull request #313 from bytecodealliance/ydnar/cm-maybe-json
cm: fix cyclical dependency on encoding/json when used in std
2 parents a0ade74 + 9145c88 commit d51afca

File tree

7 files changed

+436
-379
lines changed

7 files changed

+436
-379
lines changed

cm/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
44

5+
## Unreleased
6+
7+
### Fixed
8+
9+
- Fixed cyclical dependency on package `encoding/json` when package `cm` is used in TinyGo package `syscall`. Files that import `encoding/json` will have a `_json.go` suffix and can be excluded when this package is copied into `std`.
10+
511
## [v0.2.0] — 2025-03-15
612

713
### Added

cm/case.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package cm
22

3-
import "errors"
4-
53
// CaseUnmarshaler returns an function that can unmarshal text into
64
// [variant] or [enum] case T.
75
//
@@ -33,8 +31,7 @@ func CaseUnmarshaler[T ~uint8 | ~uint16 | ~uint32](cases []string) func(v *T, te
3331
if len(text) == 0 {
3432
return errEmpty
3533
}
36-
s := string(text)
37-
c, ok := m[s]
34+
c, ok := m[string(text)]
3835
if !ok {
3936
return errNoMatchingCase
4037
}
@@ -46,6 +43,14 @@ func CaseUnmarshaler[T ~uint8 | ~uint16 | ~uint32](cases []string) func(v *T, te
4643
const linearScanThreshold = 16
4744

4845
var (
49-
errEmpty = errors.New("empty text")
50-
errNoMatchingCase = errors.New("no matching case")
46+
errEmpty = &stringError{"empty text"}
47+
errNoMatchingCase = &stringError{"no matching case"}
5148
)
49+
50+
type stringError struct {
51+
err string
52+
}
53+
54+
func (err *stringError) Error() string {
55+
return err.err
56+
}

cm/dependencies_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//go:build !wasip1 && !wasip2 && !tinygo
2+
3+
package cm
4+
5+
import (
6+
"bytes"
7+
"os/exec"
8+
"strings"
9+
"testing"
10+
)
11+
12+
func TestDependencies(t *testing.T) {
13+
cmd := exec.Command("go", "list", "-f", "{{.Imports}}", "-tags", "module.std", ".")
14+
stdout := &bytes.Buffer{}
15+
cmd.Stdout = stdout
16+
err := cmd.Run()
17+
if err != nil {
18+
t.Error(err)
19+
return
20+
}
21+
22+
got := strings.TrimSpace(stdout.String())
23+
const want = "[structs unsafe]" // Should not include "encoding/json"
24+
if got != want {
25+
t.Errorf("Expected dependencies %s, got %s", want, got)
26+
}
27+
}

cm/list.go

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package cm
22

33
import (
4-
"bytes"
5-
"encoding/json"
64
"unsafe"
75
)
86

@@ -62,57 +60,3 @@ func (l list[T]) Data() *T {
6260
func (l list[T]) Len() uintptr {
6361
return l.len
6462
}
65-
66-
// MarshalJSON implements json.Marshaler.
67-
func (l list[T]) MarshalJSON() ([]byte, error) {
68-
if l.len == 0 {
69-
return []byte("[]"), nil
70-
}
71-
72-
s := l.Slice()
73-
var zero T
74-
if unsafe.Sizeof(zero) == 1 {
75-
// The default Go json.Encoder will marshal []byte as base64.
76-
// We override that behavior so all int types have the same serialization format.
77-
// []uint8{1,2,3} -> [1,2,3]
78-
// []uint32{1,2,3} -> [1,2,3]
79-
return json.Marshal(sliceOf(s))
80-
}
81-
return json.Marshal(s)
82-
}
83-
84-
type slice[T any] []entry[T]
85-
86-
func sliceOf[S ~[]E, E any](s S) slice[E] {
87-
return *(*slice[E])(unsafe.Pointer(&s))
88-
}
89-
90-
type entry[T any] [1]T
91-
92-
func (v entry[T]) MarshalJSON() ([]byte, error) {
93-
return json.Marshal(v[0])
94-
}
95-
96-
// UnmarshalJSON implements json.Unmarshaler.
97-
func (l *list[T]) UnmarshalJSON(data []byte) error {
98-
if bytes.Equal(data, nullLiteral) {
99-
return nil
100-
}
101-
102-
var s []T
103-
err := json.Unmarshal(data, &s)
104-
if err != nil {
105-
return err
106-
}
107-
108-
l.data = unsafe.SliceData([]T(s))
109-
l.len = uintptr(len(s))
110-
111-
return nil
112-
}
113-
114-
// nullLiteral is the JSON representation of a null literal.
115-
// By convention, to approximate the behavior of Unmarshal itself,
116-
// Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op.
117-
// See https://pkg.go.dev/encoding/json#Unmarshaler for more information.
118-
var nullLiteral = []byte("null")

cm/list_json.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//go:build !module.std
2+
3+
package cm
4+
5+
// This file contains JSON-related functionality for Component Model list types.
6+
// To avoid a cyclical dependency on package encoding/json when using this package
7+
// in a Go or TinyGo standard library, do not include files named *_json.go.
8+
9+
import (
10+
"bytes"
11+
"encoding/json"
12+
"unsafe"
13+
)
14+
15+
// MarshalJSON implements json.Marshaler.
16+
func (l list[T]) MarshalJSON() ([]byte, error) {
17+
if l.len == 0 {
18+
return []byte("[]"), nil
19+
}
20+
21+
s := l.Slice()
22+
var zero T
23+
if unsafe.Sizeof(zero) == 1 {
24+
// The default Go json.Encoder will marshal []byte as base64.
25+
// We override that behavior so all int types have the same serialization format.
26+
// []uint8{1,2,3} -> [1,2,3]
27+
// []uint32{1,2,3} -> [1,2,3]
28+
return json.Marshal(sliceOf(s))
29+
}
30+
return json.Marshal(s)
31+
}
32+
33+
type slice[T any] []entry[T]
34+
35+
func sliceOf[S ~[]E, E any](s S) slice[E] {
36+
return *(*slice[E])(unsafe.Pointer(&s))
37+
}
38+
39+
type entry[T any] [1]T
40+
41+
func (v entry[T]) MarshalJSON() ([]byte, error) {
42+
return json.Marshal(v[0])
43+
}
44+
45+
// UnmarshalJSON implements json.Unmarshaler.
46+
func (l *list[T]) UnmarshalJSON(data []byte) error {
47+
if bytes.Equal(data, nullLiteral) {
48+
return nil
49+
}
50+
51+
var s []T
52+
err := json.Unmarshal(data, &s)
53+
if err != nil {
54+
return err
55+
}
56+
57+
l.data = unsafe.SliceData([]T(s))
58+
l.len = uintptr(len(s))
59+
60+
return nil
61+
}
62+
63+
// nullLiteral is the JSON representation of a null literal.
64+
// By convention, to approximate the behavior of Unmarshal itself,
65+
// Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op.
66+
// See https://pkg.go.dev/encoding/json#Unmarshaler for more information.
67+
var nullLiteral = []byte("null")

0 commit comments

Comments
 (0)