Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions codegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ Not supported yet: outputs (DATA received from the service) need a receive loop
and unknown array element types are rejected while generating (loudly) instead of producing broken
code.

Generated RPC methods wait for the answer only when the client owns the service: with
`Options.NoClaim` or an explicit `Endpoint` they switch to `SendRPC`, because the service sends its
answer to the owner.

`char[n]` parameters are padded to `n` bytes; a longer string would be sent unchanged and silently
rejected by the firmware, so the method checks the length first and returns
`Path too long (140 bytes, max 127)`. The limit is `n - 1`, because `char[n]` holds the terminating NUL.

Identifiers are translated to the Go naming rules for initialisms (`SoundId` -> `SoundID`,
`GpsQuality` -> `GPSQuality`; see `xbot_service_interface_go/codegen/names.go`). The original name
from the `service.json` stays visible, so the schema remains the reference a reader greps for:
Expand Down
6 changes: 6 additions & 0 deletions xbot_service_interface_go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ Doc comments carry it on types, RPC methods, register setters and input senders;
RPC method whose doc comment names the schema function. So `grep` on the schema name always finds the
Go identifier, and a reader can go from `service.json` to the client without knowing the rules.

**Ownership.** A generated client waits for the RPC answer only while it owns the service. With
`Options.NoClaim` (or an explicit `Endpoint`) the service sends its answers to the owner, so the
generated methods use `SendRPC` and return the zero value instead of blocking on a reply that will
never arrive. That is what makes the SoundService usable from a tool like `soundctl`, which must not
claim the service the high-level system drives.

## Scope

The client implements what a tool like `soundctl` needs: discovery, claim, heartbeat, RPC (synchronous and
Expand Down
9 changes: 6 additions & 3 deletions xbot_service_interface_go/codegen/emit.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,14 @@ func (g *generator) decodeReturn(t, fn string) (string, []string, error) {
}

// zeroValue is the Go zero value of a return type, used in the error path.
// zeroValue is the Go zero value of a generated return type: "" for strings, nil
// for slices ([]byte like the uint8_t arrays, and the numeric array types such as
// []float64) and 0 for everything else (numeric scalars and enums).
func zeroValue(goType string) string {
switch goType {
case "string":
switch {
case goType == "string":
return `""`
case "[]byte":
case strings.HasPrefix(goType, "[]"):
return "nil"
}
return "0"
Expand Down
84 changes: 64 additions & 20 deletions xbot_service_interface_go/codegen/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@ func (g *generator) run() error {
}

g.line("// Client is a client for the " + s.Type + " service.")
g.line("type Client struct{ conn *xbot.Conn }")
g.line("type Client struct {")
g.line("\tconn *xbot.Conn")
g.line("\t// sendOnly is set when this client does not own the service (NoClaim or")
g.line("\t// Endpoint): the service sends its RPC answers to the owner, so the calls")
g.line("\t// are fire and forget.")
g.line("\tsendOnly bool")
g.line("}")
g.line("")
g.line("// Dial connects to the service; it claims it unless opts.NoClaim is set.")
if g.opts.ServiceID != 0 {
Expand All @@ -76,7 +82,7 @@ func (g *generator) run() error {
g.line("\tif err != nil {")
g.line("\t\treturn nil, err")
g.line("\t}")
g.line("\treturn &Client{conn: conn}, nil")
g.line("\treturn &Client{conn: conn, sendOnly: opts.NoClaim || opts.Endpoint != \"\"}, nil")
g.line("}")
g.line("")
g.line("// Close closes the connection.")
Expand Down Expand Up @@ -162,11 +168,21 @@ func (g *generator) emitEnum(e Enum) error {
return nil
}

// charCheck is a char[n] parameter that needs a length check before it is packed:
// byteParam pads short values but sends longer ones unchanged, and the firmware
// then rejects them without a word, so the check turns that into a clear error.
type charCheck struct {
param string
schema string
length int
}

// emitFunction writes one RPC method.
func (g *generator) emitFunction(fn Function) error {
name := goName(fn.Name)
sig := []string{"ctx context.Context"}
var args []string
var params []string
var charChecks []charCheck
for _, p := range fn.Parameters {
pname := goName(p.Name)
where := "parameter " + pname + " of " + name
Expand All @@ -178,39 +194,67 @@ func (g *generator) emitFunction(fn Function) error {
if err != nil {
return err
}
if base, length, _ := splitType(p.Type); base == "char" && length > 0 {
charChecks = append(charChecks, charCheck{param: pname, schema: p.Type, length: length})
}
sig = append(sig, pname+" "+gt)
args = append(args, fmt.Sprintf("\t\txbot.Bytes(%d, %s),", p.ID, expr))
params = append(params, fmt.Sprintf("xbot.Bytes(%d, %s),", p.ID, expr))
}
retType, lines, err := g.decodeReturn(fn.ReturnType, name)
if err != nil {
return err
}
ret := "error"
zero := ""
if retType != "" {
ret = "(" + retType + ", error)"
zero = zeroValue(retType) + ", "
}

// call emits one call to the service, indented by indent, using callName as
// the method on the connection (Call or SendRPC).
call := func(indent, assign, callName string) {
ctxArg := "ctx, "
if callName == "SendRPC" {
ctxArg = ""
}
if len(params) > 0 {
g.f("%s%sc.conn.%s(%sFn%s,", indent, assign, callName, ctxArg, name)
for _, param := range params {
g.line(indent + " " + param)
}
g.line(indent + ")")
return
}
g.f("%s%sc.conn.%s(%sFn%s)", indent, assign, callName, ctxArg, name)
}

g.f("// %s calls the RPC function %d%s.", name, fn.ID, schemaRef(fn.Name, name))
g.line("//")
g.line("// It only waits for the answer when this client owns the service. Without a claim")
g.line("// (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner,")
g.line("// so the call is fire and forget and returns the zero value.")
g.f("func (c *Client) %s(%s) %s {", name, strings.Join(sig, ", "), ret)
assign := "\tpayload, err := "
if retType == "" {
assign = "\t_, err := "
}
if len(args) > 0 {
g.f("%sc.conn.Call(ctx, Fn%s,", assign, name)
for _, a := range args {
g.line(a)
}
g.line("\t)")
} else {
g.f("%sc.conn.Call(ctx, Fn%s)", assign, name)
for _, c := range charChecks {
g.f("\tif len(%s) >= %d { // %s includes the NUL", c.param, c.length, c.schema)
g.f("\t\treturn %serrf(\"%s too long (%%d bytes, max %%d)\", len(%s), %d)",
zero, c.param, c.param, c.length-1)
g.line("\t}")
}

g.line(" if c.sendOnly {")
if retType == "" {
g.line("\treturn err")
call(" ", "return ", "SendRPC")
g.line(" }")
call(" ", "_, err := ", "Call")
g.line(" return err")
} else {
g.line("\tif err != nil {")
g.f("\t\treturn %s, err", zeroValue(retType))
g.line("\t}")
call(" ", "return "+zero, "SendRPC")
g.line(" }")
call(" ", "payload, err := ", "Call")
g.line(" if err != nil {")
g.f(" return %s, err", zeroValue(retType))
g.line(" }")
for _, l := range lines {
g.line(strings.Replace(l, "ZERO", zeroValue(retType), 1))
}
Expand Down
99 changes: 97 additions & 2 deletions xbot_service_interface_go/codegen/testdata/demo_service.golden.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions xbot_service_interface_go/codegen/testdata/demo_service.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@
}
],
"return_type": "uint8_t"
},
{
"id": 4,
"name": "NumericArrayReturn",
"parameters": [],
"return_type": "double[4]"
}
],
"inputs": [
Expand Down
Loading
Loading