Skip to content

Commit

Permalink
feat(go): support direct implementation of jsii interfaces (aws#2614)
Browse files Browse the repository at this point in the history
Pure go implementations of jsii interfaces are detected upon being
passed as a parameter to a JavaScript call. When this happens, a
`create` call is sent to the `@jsii/kernel` process with the correct
list of implemented interface FQNs, and all necessary overrides records.
The object is then registered into the new `ObjectStore` for later
reference.

When a callback request interrupts the normal request/response flow, the
`kernel.Client` will handle the callback by getting the appropriate
receiver object, invoking the designated go implementation, and sending
the result to the `@jsii/kernel` process before resuming normal response
handling.

Related to aws#2048 (partial support)
  • Loading branch information
RomainMuller authored Mar 3, 2021
1 parent 19cbd25 commit 9da3282
Show file tree
Hide file tree
Showing 43 changed files with 3,088 additions and 1,534 deletions.
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ updates:
labels:
- dependencies
- language/go
ignore:
- dependency-name: github.com/aws/jsii-runtime-go
- dependency-name: github.com/aws/jsii-runtime-go/*
- dependency-name: github.com/aws/jsii/jsii-calc/go/*

- package-ecosystem: gomod
directory: '/packages/@jsii/go-runtime/jsii-runtime-go'
Expand All @@ -64,6 +68,10 @@ updates:
labels:
- dependencies
- language/go
ignore:
- dependency-name: github.com/aws/jsii-runtime-go
- dependency-name: github.com/aws/jsii-runtime-go/*
- dependency-name: github.com/aws/jsii/jsii-calc/go/*

- package-ecosystem: github-actions
directory: '/'
Expand Down
2 changes: 1 addition & 1 deletion packages/@jsii/go-runtime/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
*.js
*.d.ts

!jsii-runtime-go/kernel/process/jsii-mock-runtime.js
!jsii-runtime-go/internal/kernel/process/jsii-mock-runtime.js
6 changes: 3 additions & 3 deletions packages/@jsii/go-runtime/build-tools/all-go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ function visit(dir: string): void {
continue;
}
if (file === 'go.mod') {
const args = process.argv.slice(2);
console.error(`$ go ${args.join(' ')} # ${path}`);
const [cmd, ...args] = process.argv.slice(2);
console.error(`$ ${cmd} ${args.join(' ')} # ${path}`);
try {
runCommand('go', args, { cwd: dir, stdio: 'inherit' });
runCommand(cmd, args, { cwd: dir, stdio: 'inherit' });
} catch (e) {
console.error(e.message);
process.exit(-1);
Expand Down
32 changes: 32 additions & 0 deletions packages/@jsii/go-runtime/build-tools/go-run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { spawnSync } from 'child_process';
import { constants } from 'os';
import { join } from 'path';
import { argv, exit } from 'process';

import { runCommand } from './_constants';

if (argv.length < 3) {
console.error(`Not enough arguments: ${argv[0]} ${argv[1]} <cmd> [args]`);
exit(1);
}

const [, , cmd, ...args] = argv;

const result = spawnSync('go', ['env', 'GOPATH'], {
stdio: ['inherit', 'pipe', 'inherit'],
});

if (result.error) {
console.error('"go env GOPATH" failed:', result.error);
exit(-1);
}

if (result.status !== 0) {
console.error('"go env GOPATH" failed:', result.status ?? result.signal);
exit(
result.status ?? 128 + constants.signals[result.signal as NodeJS.Signals],
);
}

const gopath = result.stdout.toString('utf8').trim();
runCommand(join(gopath, 'bin', cmd), args);
2 changes: 2 additions & 0 deletions packages/@jsii/go-runtime/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ require (
github.com/aws/jsii-runtime-go v0.0.0
github.com/aws/jsii/jsii-calc/go/jsiicalc/v3 v3.20.120
github.com/aws/jsii/jsii-calc/go/scopejsiicalclib v0.0.0
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5
golang.org/x/tools v0.1.0
)

replace (
Expand Down
35 changes: 35 additions & 0 deletions packages/@jsii/go-runtime/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,37 @@
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
26 changes: 26 additions & 0 deletions packages/@jsii/go-runtime/jsii-calc-test/callbacks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"testing"

calc "github.com/aws/jsii/jsii-calc/go/jsiicalc/v3"
)

func TestPureInterfacesCanBeUsedTransparently(t *testing.T) {
expected := calc.StructB{RequiredString: "It's Britney b**ch!"}
delegate := &StructReturningDelegate{expected: expected}
consumer := calc.NewConsumePureInterface(delegate)
actual := consumer.WorkItBaby()

if actual != expected {
t.Errorf("Expected %v; actual: %v", expected, actual)
}
}

type StructReturningDelegate struct {
expected calc.StructB
}

func (o *StructReturningDelegate) ReturnStruct() calc.StructB {
return o.expected
}
33 changes: 4 additions & 29 deletions packages/@jsii/go-runtime/jsii-runtime-go/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@ func (o override) isOverride() {
type MethodOverride struct {
override

Method *string `json:"method"`
Cookie *string `json:"cookie"`
JsiiMethod string `json:"method"`
GoMethod string `json:"cookie"`
}

// PropertyOverride is used to register a "go-native" implementation to be
// substituted to the default javascript implementation on the created object.
type PropertyOverride struct {
override

Property *string `json:"property"`
Cookie *string `json:"cookie"`
JsiiProperty string `json:"property"`
GoGetter string `json:"cookie"`
}

func IsMethodOverride(value Override) bool {
Expand Down Expand Up @@ -73,28 +73,3 @@ type StructDescriptor struct {
FQN FQN `json:"fqn"`
Fields map[string]interface{} `json:"data"`
}

type Callback struct {
CallbackID *string `json:"cbid"`
Cookie *string `json:"cookie"`
Invoke InvokeCallback `json:"invoke"`
Get GetCallback `json:"get"`
Set SetCallback `json:"set"`
}

type InvokeCallback struct {
Method string `json:"method"`
Arguments []interface{} `json:"args"`
ObjRef ObjectRef `json:"objref"`
}

type GetCallback struct {
Property string `json:"property"`
ObjRef ObjectRef `json:"objref"`
}

type SetCallback struct {
Property string `json:"property"`
Value interface{} `json:"value"`
ObjRef ObjectRef `json:"objref"`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package embedded contains the embedded @jsii/kernel code, which is used
// unless the JSII_RUNTIME environment variable is set to a different program.
package embedded
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ var embeddedFS embed.FS
var entrypointName string = path.Join("bin", "jsii-runtime.js")

// ExtractRuntime extracts a copy of the embedded runtime library into
// the designated directory.
// the designated directory, and returns the fully qualified path to the entry
// point to be used when starting the child process.
func ExtractRuntime(into string) (entrypoint string, err error) {
err = extractRuntime(into, embeddedRootDir)
if err == nil {
Expand All @@ -25,8 +26,8 @@ func ExtractRuntime(into string) (entrypoint string, err error) {
return
}

// extractRuntime copies the contents of embeddedFS at "from" to the provided "into"
// directory, recursively.
// extractRuntime copies the contents of embeddedFS at "from" to the provided
// "into" directory, recursively.
func extractRuntime(into string, from string) error {
files, err := embeddedFS.ReadDir(from)
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type BeginResponse struct {
PromiseID *string `json:"promise_id"`
}

func (c *client) Begin(props BeginProps) (response BeginResponse, err error) {
func (c *Client) Begin(props BeginProps) (response BeginResponse, err error) {
type request struct {
kernelRequest
BeginProps
Expand Down
143 changes: 137 additions & 6 deletions packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/callbacks.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,146 @@
package kernel

import (
"fmt"
"reflect"

"github.com/aws/jsii-runtime-go/internal/api"
)

type CallbacksResponse struct {
kernelResponse
Callbacks []api.Callback `json:"callbacks"`
type callback struct {
CallbackID string `json:"cbid"`
Cookie string `json:"cookie"`
Invoke *invokeCallback `json:"invoke"`
Get *getCallback `json:"get"`
Set *setCallback `json:"set"`
}

func (c *client) Callbacks() (response CallbacksResponse, err error) {
err = c.request(kernelRequest{"callbacks"}, &response)
return
func (c *callback) handle(result kernelResponder) error {
var (
retval reflect.Value
err error
)
if c.Invoke != nil {
retval, err = c.Invoke.handle(c.Cookie)
} else if c.Get != nil {
retval, err = c.Get.handle(c.Cookie)
} else if c.Set != nil {
retval, err = c.Set.handle(c.Cookie)
} else {
return fmt.Errorf("invalid callback object: %v", c)
}

type callbackResult struct {
CallbackID string `json:"cbid"`
Result interface{} `json:"result,omitempty"`
Error string `json:"err,omitempty"`
}
type completeRequest struct {
kernelRequester
callbackResult `json:"complete"`
}

client := GetClient()
request := completeRequest{}
request.CallbackID = c.CallbackID
request.Result = client.CastPtrToRef(retval)
if err != nil {
request.Error = err.Error()
}
return client.request(request, result)
}

type invokeCallback struct {
Method string `json:"method"`
Arguments []interface{} `json:"args"`
ObjRef api.ObjectRef `json:"objref"`
}

func (i *invokeCallback) handle(cookie string) (retval reflect.Value, err error) {
client := GetClient()

receiver := reflect.ValueOf(client.GetObject(i.ObjRef))
method := receiver.MethodByName(cookie)

return client.invoke(method, i.Arguments)
}

type getCallback struct {
Property string `json:"property"`
ObjRef api.ObjectRef `json:"objref"`
}

func (g *getCallback) handle(cookie string) (retval reflect.Value, err error) {
client := GetClient()

receiver := reflect.ValueOf(client.GetObject(g.ObjRef))
method := receiver.MethodByName(cookie)

return client.invoke(method, nil)
}

type setCallback struct {
Property string `json:"property"`
Value interface{} `json:"value"`
ObjRef api.ObjectRef `json:"objref"`
}

func (s *setCallback) handle(cookie string) (retval reflect.Value, err error) {
client := GetClient()

receiver := reflect.ValueOf(client.GetObject(s.ObjRef))
method := receiver.MethodByName(fmt.Sprintf("Set%v", cookie))

return client.invoke(method, []interface{}{s.Value})
}

func (c *Client) invoke(method reflect.Value, args []interface{}) (retval reflect.Value, err error) {
if !method.IsValid() {
err = fmt.Errorf("invalid method")
return
}

// Convert the arguments, if any...
callArgs := make([]reflect.Value, len(args))
methodType := method.Type()
numIn := methodType.NumIn()
for i, arg := range args {
var argType reflect.Type
if i < numIn {
argType = methodType.In(i)
} else if methodType.IsVariadic() {
argType = methodType.In(i - 1)
} else {
err = fmt.Errorf("too many arguments received %d for %d", len(args), numIn)
return
}
callArgs[i] = reflect.New(argType)
c.CastAndSetToPtr(arg, callArgs[i].Interface())
}

// Ready to catch an error if the method panics...
defer func() {
if r := recover(); r != nil {
if err == nil {
var ok bool
if err, ok = r.(error); !ok {
err = fmt.Errorf("%v", r)
}
} else {
// This is not expected - so we panic!
panic(r)
}
}
}()

result := method.Call(callArgs)
switch len(result) {
case 0:
retval = reflect.ValueOf(nil)
case 1:
retval = result[0]
default:
err = fmt.Errorf("too many return values: %v", result)
}
return
}
Loading

0 comments on commit 9da3282

Please sign in to comment.