aboutsummaryrefslogtreecommitdiff
path: root/xC-gen-proto-go.awk
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2022-08-08 04:39:20 +0200
committerPřemysl Eric Janouch <p@janouch.name>2022-09-05 14:26:00 +0200
commit1639235a48dbed75c2563c9a497b41c31a2a1bae (patch)
tree18193b72fa47e6bcac1358289ac9c36ed00c70ac /xC-gen-proto-go.awk
parent2160d037943ef0a3adbf4c6e30a91ee0f205c3f3 (diff)
downloadxK-1639235a48dbed75c2563c9a497b41c31a2a1bae.tar.gz
xK-1639235a48dbed75c2563c9a497b41c31a2a1bae.tar.xz
xK-1639235a48dbed75c2563c9a497b41c31a2a1bae.zip
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options, it was decided to implement an XDR-like protocol code generator in portable AWK. It now has two backends, per each of: - xF, the X11 frontend, is in C, and is meant to be the primary user interface in the future. - xP, the web frontend, relies on a protocol proxy written in Go, and is meant for use on-the-go (no pun intended). They are very much work-in-progress proofs of concept right now, and the relay protocol is certain to change.
Diffstat (limited to 'xC-gen-proto-go.awk')
-rw-r--r--xC-gen-proto-go.awk447
1 files changed, 447 insertions, 0 deletions
diff --git a/xC-gen-proto-go.awk b/xC-gen-proto-go.awk
new file mode 100644
index 0000000..1880de7
--- /dev/null
+++ b/xC-gen-proto-go.awk
@@ -0,0 +1,447 @@
+# xC-gen-proto-go.awk: Go backend for xC-gen-proto.awk.
+#
+# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
+# SPDX-License-Identifier: 0BSD
+#
+# This backend also enables proxying to other endpoints using JSON.
+
+function define_internal(name, gotype) {
+ Types[name] = "internal"
+ CodegenGoType[name] = gotype
+}
+
+function define_sint(size, shortname, gotype) {
+ shortname = "i" size
+ gotype = "int" size
+ define_internal(shortname, gotype)
+
+ if (size == 8) {
+ CodegenSerialize[shortname] = "\tdata = append(data, uint8(%s))\n"
+ CodegenDeserialize[shortname] = \
+ "\tif len(data) >= 1 {\n" \
+ "\t\t%s, data = int8(data[0]), data[1:]\n" \
+ "\t} else {\n" \
+ "\t\treturn nil, false\n" \
+ "\t}\n"
+ return
+ }
+
+ CodegenSerialize[shortname] = \
+ "\tdata = binary.BigEndian.AppendUint" size "(data, uint" size "(%s))\n"
+ CodegenDeserialize[shortname] = \
+ "\tif len(data) >= " (size / 8) " {\n" \
+ "\t\t%s = " gotype "(binary.BigEndian.Uint" size "(data))\n" \
+ "\t\tdata = data[" (size / 8) ":]\n" \
+ "\t} else {\n" \
+ "\t\treturn nil, false\n" \
+ "\t}\n"
+}
+
+function define_uint(size, shortname, gotype) {
+ shortname = "u" size
+ gotype = "uint" size
+ define_internal(shortname, gotype)
+
+ # Both byte and uint8 luckily marshal as base64-encoded JSON strings.
+ if (size == 8) {
+ CodegenSerialize[shortname] = "\tdata = append(data, %s)\n"
+ CodegenDeserialize[shortname] = \
+ "\tif len(data) >= 1 {\n" \
+ "\t\t%s, data = data[0], data[1:]\n" \
+ "\t} else {\n" \
+ "\t\treturn nil, false\n" \
+ "\t}\n"
+ return
+ }
+
+ CodegenSerialize[shortname] = \
+ "\tdata = binary.BigEndian.AppendUint" size "(data, %s)\n"
+ CodegenDeserialize[shortname] = \
+ "\tif len(data) >= " (size / 8) " {\n" \
+ "\t\t%s = binary.BigEndian.Uint" size "(data)\n" \
+ "\t\tdata = data[" (size / 8) ":]\n" \
+ "\t} else {\n" \
+ "\t\treturn nil, false\n" \
+ "\t}\n"
+}
+
+function codegen_begin() {
+ define_sint("8")
+ define_sint("16")
+ define_sint("32")
+ define_sint("64")
+ define_uint("8")
+ define_uint("16")
+ define_uint("32")
+ define_uint("64")
+
+ define_internal("bool", "bool")
+ CodegenSerialize["bool"] = \
+ "\tif %s {\n" \
+ "\t\tdata = append(data, 1)\n" \
+ "\t} else {\n" \
+ "\t\tdata = append(data, 0)\n" \
+ "\t}\n"
+ CodegenDeserialize["bool"] = \
+ "\tif data, ok = protoConsumeBoolFrom(data, &%s); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+
+ define_internal("string", "string")
+ CodegenSerialize["string"] = \
+ "\tif data, ok = protoAppendStringTo(data, %s); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+ CodegenDeserialize["string"] = \
+ "\tif data, ok = protoConsumeStringFrom(data, &%s); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+
+ print "package main"
+ print ""
+ print "import ("
+ print "\t`encoding/binary`"
+ print "\t`encoding/json`"
+ print "\t`errors`"
+ print "\t`math`"
+ print "\t`strconv`"
+ print "\t`unicode/utf8`"
+ print ")"
+ print ""
+
+ print "// protoConsumeBoolFrom tries to deserialize a boolean value"
+ print "// from the beginning of a byte stream. When successful,"
+ print "// it returns a subslice with any data that might follow."
+ print "func protoConsumeBoolFrom(data []byte, b *bool) ([]byte, bool) {"
+ print "\tif len(data) < 1 {"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\tif data[0] != 0 {"
+ print "\t\t*b = true"
+ print "\t} else {"
+ print "\t\t*b = false"
+ print "\t}"
+ print "\treturn data[1:], true"
+ print "}"
+ print ""
+
+ print "// protoAppendStringTo tries to serialize a string value,"
+ print "// appending it to the end of a byte stream."
+ print "func protoAppendStringTo(data []byte, s string) ([]byte, bool) {"
+ print "\tif len(s) > math.MaxUint32 {"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\tdata = binary.BigEndian.AppendUint32(data, uint32(len(s)))"
+ print "\treturn append(data, s...), true"
+ print "}"
+ print ""
+
+ print "// protoConsumeStringFrom tries to deserialize a string value"
+ print "// from the beginning of a byte stream. When successful,"
+ print "// it returns a subslice with any data that might follow."
+ print "func protoConsumeStringFrom(data []byte, s *string) ([]byte, bool) {"
+ print "\tif len(data) < 4 {"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\tlength := binary.BigEndian.Uint32(data)"
+ print "\tif data = data[4:]; uint64(len(data)) < uint64(length) {"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\t*s = string(data[:length])"
+ print "\tif !utf8.ValidString(*s) {"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\treturn data[length:], true"
+ print "}"
+ print ""
+
+ print "// protoUnmarshalEnumJSON converts a JSON fragment to an integer,"
+ print "// ensuring that it's within the expected range of enum values."
+ print "func protoUnmarshalEnumJSON(data []byte) (int64, error) {"
+ print "\tvar n int64"
+ print "\tif err := json.Unmarshal(data, &n); err != nil {"
+ print "\t\treturn 0, err"
+ print "\t} else if n > math.MaxInt32 || n < math.MinInt32 {"
+ print "\t\treturn 0, errors.New(`integer out of range`)"
+ print "\t} else {"
+ print "\t\treturn n, nil"
+ print "\t}"
+ print "}"
+ print ""
+}
+
+function codegen_constant(name, value) {
+ print "const " PrefixCamel snaketocamel(name) " = " value
+ print ""
+}
+
+function codegen_enum_value(name, subname, value, cg, goname) {
+ goname = PrefixCamel name snaketocamel(subname)
+ append(cg, "fields",
+ "\t" goname " = " value "\n")
+ append(cg, "stringer",
+ "\tcase " goname ":\n" \
+ "\t\treturn `" snaketocamel(subname) "`\n")
+ append(cg, "marshal",
+ goname ",\n")
+ append(cg, "unmarshal",
+ "\tcase `" snaketocamel(subname) "`:\n" \
+ "\t\t*v = " goname "\n")
+}
+
+function codegen_enum(name, cg, gotype, fields) {
+ gotype = PrefixCamel name
+ print "type " gotype " int"
+ print ""
+
+ print "const ("
+ print cg["fields"] ")"
+ print ""
+
+ print "func (v " gotype ") String() string {"
+ print "\tswitch v {"
+ print cg["stringer"] "\tdefault:"
+ print "\t\treturn strconv.Itoa(int(v))"
+ print "\t}"
+ print "}"
+ print ""
+
+ fields = cg["marshal"]
+ sub(/,\n$/, ":", fields)
+ gsub(/\n/, "\n\t", fields)
+ print "func (v " gotype ") MarshalJSON() ([]byte, error) {"
+ print "\tswitch v {"
+ print indent("case " fields)
+ print "\t\treturn json.Marshal(v.String())"
+ print "\t}"
+ print "\treturn json.Marshal(int(v))"
+ print "}"
+ print ""
+
+ print "func (v *" gotype ") UnmarshalJSON(data []byte) error {"
+ print "\tvar s string"
+ print "\tif json.Unmarshal(data, &s) == nil {"
+ print "\t\t// Handled below."
+ print "\t} else if n, err := protoUnmarshalEnumJSON(data); err != nil {"
+ print "\t\treturn err"
+ print "\t} else {"
+ print "\t\t*v = " gotype "(n)"
+ print "\t\treturn nil"
+ print "\t}"
+ print ""
+ print "\tswitch s {"
+ print cg["unmarshal"] "\tdefault:"
+ print "\t\treturn errors.New(`unrecognized value: ` + s)"
+ print "\t}"
+ print "\treturn nil"
+ print "}"
+ print ""
+
+ # XXX: This should also check if it isn't out-of-range for any reason,
+ # but our usage of sprintf() stands in the way a bit.
+ CodegenSerialize[name] = \
+ "\tdata = binary.BigEndian.AppendUint32(data, uint32(%s))\n"
+ CodegenDeserialize[name] = \
+ "\tif len(data) >= 4 {\n" \
+ "\t\t%s = " gotype "(int32(binary.BigEndian.Uint32(data)))\n" \
+ "\t\tdata = data[4:]\n" \
+ "\t} else {\n" \
+ "\t\treturn nil, false\n" \
+ "\t}\n"
+
+ CodegenGoType[name] = gotype
+ for (i in cg)
+ delete cg[i]
+}
+
+function codegen_struct_field(d, cg, camel, f, serialize, deserialize) {
+ camel = snaketocamel(d["name"])
+ f = "s." camel
+ serialize = CodegenSerialize[d["type"]]
+ deserialize = CodegenDeserialize[d["type"]]
+ if (!d["isarray"]) {
+ append(cg, "fields", "\t" camel " " CodegenGoType[d["type"]] \
+ " `json:\"" decapitalize(camel) "\"`\n")
+ append(cg, "serialize", sprintf(serialize, f))
+ append(cg, "deserialize", sprintf(deserialize, f))
+ return
+ }
+
+ append(cg, "fields", "\t" camel " []" CodegenGoType[d["type"]] \
+ " `json:\"" decapitalize(camel) "\"`\n")
+
+ # XXX: This should also check if it isn't out-of-range for any reason.
+ append(cg, "serialize",
+ sprintf(CodegenSerialize["u32"], "uint32(len(" f "))"))
+ if (d["type"] == "u8") {
+ append(cg, "serialize",
+ "\tdata = append(data, " f "...)\n")
+ } else {
+ append(cg, "serialize",
+ "\tfor i := 0; i < len(" f "); i++ {\n" \
+ indent(sprintf(serialize, f "[i]")) \
+ "\t}\n")
+ }
+
+ append(cg, "deserialize",
+ "\t{\n" \
+ "\t\tvar length uint32\n" \
+ indent(sprintf(CodegenDeserialize["u32"], "length")))
+ if (d["type"] == "u8") {
+ append(cg, "deserialize",
+ "\t\tif uint64(len(data)) < uint64(length) {\n" \
+ "\t\t\treturn nil, false\n" \
+ "\t\t}\n" \
+ "\t\t" f ", data = data[:length], data[length:]\n" \
+ "\t}\n")
+ } else {
+ append(cg, "deserialize",
+ "\t\t" f " = make([]" CodegenGoType[d["type"]] ", length)\n" \
+ "\t}\n" \
+ "\tfor i := 0; i < len(" f "); i++ {\n" \
+ indent(sprintf(deserialize, f "[i]")) \
+ "\t}\n")
+ }
+}
+
+function codegen_struct_tag(d, cg, camel, f) {
+ camel = snaketocamel(d["name"])
+ f = "s." camel
+ append(cg, "fields", "\t" camel " " CodegenGoType[d["type"]] \
+ " `json:\"" decapitalize(camel) "\"`\n")
+ append(cg, "serialize", sprintf(CodegenSerialize[d["type"]], f))
+ # Do not deserialize here, that is already done by the containing union.
+}
+
+function codegen_struct(name, cg, gotype) {
+ gotype = PrefixCamel name
+ print "type " gotype " struct {\n" cg["fields"] "}\n"
+
+ if (cg["serialize"]) {
+ print "func (s *" gotype ") AppendTo(data []byte) ([]byte, bool) {"
+ print "\tok := true"
+ print cg["serialize"] "\treturn data, ok"
+ print "}"
+ print ""
+
+ CodegenSerialize[name] = \
+ "\tif data, ok = %s.AppendTo(data); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+ }
+ if (cg["deserialize"]) {
+ print "func (s *" gotype ") ConsumeFrom(data []byte) ([]byte, bool) {"
+ print "\tok := true"
+ print cg["deserialize"] "\treturn data, ok"
+ print "}"
+ print ""
+
+ CodegenDeserialize[name] = \
+ "\tif data, ok = %s.ConsumeFrom(data); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+ }
+
+ CodegenGoType[name] = gotype
+ for (i in cg)
+ delete cg[i]
+}
+
+function codegen_union_tag(d, cg) {
+ cg["tagtype"] = d["type"]
+ cg["tagname"] = d["name"]
+ # The tag is implied from the type of struct stored in the interface.
+}
+
+function codegen_union_struct(name, casename, cg, scg, structname, init) {
+ # And thus not all generated structs are present in Types.
+ structname = name snaketocamel(casename)
+ codegen_struct(structname, scg)
+
+ init = CodegenGoType[structname] "{" snaketocamel(cg["tagname"]) \
+ ": " decapitalize(snaketocamel(cg["tagname"])) "}"
+ append(cg, "unmarshal",
+ "\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \
+ "\t\ts := " init "\n" \
+ "\t\terr = json.Unmarshal(data, &s)\n" \
+ "\t\tu.Interface = s\n")
+ append(cg, "serialize",
+ "\tcase " CodegenGoType[structname] ":\n" \
+ indent(sprintf(CodegenSerialize[structname], "union")))
+ append(cg, "deserialize",
+ "\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \
+ "\t\ts := " init "\n" \
+ indent(sprintf(CodegenDeserialize[structname], "s")) \
+ "\t\tu.Interface = s\n")
+}
+
+function codegen_union(name, cg, gotype, tagfield, tagvar) {
+ gotype = PrefixCamel name
+ print "type " gotype " struct {"
+ print "\tInterface any"
+ print "}"
+ print ""
+
+ print "func (u *" gotype ") MarshalJSON() ([]byte, error) {"
+ print "\treturn json.Marshal(u.Interface)"
+ print "}"
+ print ""
+
+ tagfield = snaketocamel(cg["tagname"])
+ tagvar = decapitalize(tagfield)
+ print "func (u *" gotype ") UnmarshalJSON(data []byte) (err error) {"
+ print "\tvar t struct {"
+ print "\t\t" tagfield " " CodegenGoType[cg["tagtype"]] \
+ " `json:\"" tagvar "\"`"
+ print "\t}"
+ print "\tif err := json.Unmarshal(data, &t); err != nil {"
+ print "\t\treturn err"
+ print "\t}"
+ print ""
+ print "\tswitch " tagvar " := t." tagfield "; " tagvar " {"
+ print cg["unmarshal"] "\tdefault:"
+ print "\t\terr = errors.New(`unsupported value: ` + " tagvar ".String())"
+ print "\t}"
+ print "\treturn err"
+ print "}"
+ print ""
+
+ # XXX: Consider changing the interface into an AppendTo/ConsumeFrom one,
+ # that would eliminate these type case switches entirely.
+ # On the other hand, it would make it possible to send unsuitable structs.
+ print "func (u *" gotype ") AppendTo(data []byte) ([]byte, bool) {"
+ print "\tok := true"
+ print "\tswitch union := u.Interface.(type) {"
+ print cg["serialize"] "\tdefault:"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\treturn data, ok"
+ print "}"
+ print ""
+
+ CodegenSerialize[name] = \
+ "\tif data, ok = %s.AppendTo(data); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+
+ print "func (u *" gotype ") ConsumeFrom(data []byte) ([]byte, bool) {"
+ print "\tok := true"
+ print "\tvar " tagvar " " CodegenGoType[cg["tagtype"]]
+ print sprintf(CodegenDeserialize[cg["tagtype"]], tagvar)
+ print "\tswitch " tagvar " {"
+ print cg["deserialize"] "\tdefault:"
+ print "\t\treturn nil, false"
+ print "\t}"
+ print "\treturn data, ok"
+ print "}"
+ print ""
+
+ CodegenDeserialize[name] = \
+ "\tif data, ok = %s.ConsumeFrom(data); !ok {\n" \
+ "\t\treturn nil, ok\n" \
+ "\t}\n"
+
+ CodegenGoType[name] = gotype
+ for (i in cg)
+ delete cg[i]
+}