Skip to content

Commit

Permalink
feat(go): support direct implementation of jsii interfaces
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.
  • Loading branch information
RomainMuller committed Mar 1, 2021
1 parent c2bd156 commit 95cf675
Show file tree
Hide file tree
Showing 26 changed files with 3,613 additions and 1,790 deletions.
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/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 @@ -64,28 +64,3 @@ type EnumRef struct {
type WireMap struct {
MapData map[string]interface{} `json:"$jsii.map"`
}

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"`
}
144 changes: 135 additions & 9 deletions packages/@jsii/go-runtime/jsii-runtime-go/kernel/callbacks.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,146 @@
package kernel

import "github.com/aws/jsii-runtime-go/api"
import (
"fmt"
"reflect"

type callbacksRequest struct {
kernelRequester
"github.com/aws/jsii-runtime-go/api"
)

API string `json:"api"`
type callback struct {
CallbackID string `json:"cbid"`
Cookie string `json:"cookie"`
Invoke *invokeCallback `json:"invoke"`
Get *getCallback `json:"get"`
Set *setCallback `json:"set"`
}

type CallbacksResponse struct {
kernelResponder
func (c *callback) handle(result kernelResponse) 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)
}

Callbacks []api.Callback `json:"callbacks"`
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 (c *client) Callbacks() (response CallbacksResponse, err error) {
err = c.request(callbacksRequest{API: "callbacks"}, &response)
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
}
85 changes: 29 additions & 56 deletions packages/@jsii/go-runtime/jsii-runtime-go/kernel/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import (
"runtime"
"sync"

"github.com/aws/jsii-runtime-go/api"
"github.com/aws/jsii-runtime-go/embedded"
"github.com/aws/jsii-runtime-go/objectstore"
"github.com/aws/jsii-runtime-go/typeregistry"
)

Expand All @@ -37,8 +39,9 @@ type client struct {
stdin io.WriteCloser
tmpdir string

types typeregistry.TypeRegistry
objects map[reflect.Value]string
types typeregistry.TypeRegistry

objects objectstore.ObjectStore
}

// GetClient returns a singleton client instance, initializing one the first
Expand Down Expand Up @@ -85,7 +88,7 @@ func CloseClient() {
// was correct.
func newClient() (*client, error) {
clientinstance := &client{
objects: make(map[reflect.Value]string),
objects: objectstore.NewObjectStore(),
types: typeregistry.NewTypeRegistry(),
}

Expand Down Expand Up @@ -199,53 +202,7 @@ func (c *client) Types() typeregistry.TypeRegistry {
}

func (c *client) RegisterInstance(instance reflect.Value, instanceID string) error {
instance = reflect.Indirect(instance)
if instance.Kind() == reflect.Interface {
instance = reflect.ValueOf(instance.Interface()).Elem()
}

if existing, found := c.objects[instance]; found && existing != instanceID {
return fmt.Errorf("attempted to register %v as %s, but it was already registered as %s", instance, instanceID, existing)
}

var findAliases func(v reflect.Value) []reflect.Value
findAliases = func(v reflect.Value) (res []reflect.Value) {
v = reflect.Indirect(v)
t := v.Type()
numField := t.NumField()
for i := 0; i < numField; i++ {
f := t.Field(i)
if f.Name == "_" {
// Ignore any padding
continue
}
if !f.Anonymous {
// Ignore any non-anonymous field
continue
}
fv := reflect.Indirect(v.Field(i))
if fv.Kind() == reflect.Interface {
fv = reflect.ValueOf(fv.Interface()).Elem()
}

res = append(res, fv)
res = append(res, findAliases(fv)...)
}
return
}

aliases := findAliases(instance)
for _, alias := range aliases {
if existing, found := c.objects[alias]; found && existing != instanceID {
return fmt.Errorf("value %v is embedded in %s, but was already assigned %s", alias, instanceID, existing)
}
}

c.objects[instance] = instanceID
for _, alias := range aliases {
c.objects[alias] = instanceID
}
return nil
return c.objects.Register(instance, instanceID)
}

func (c *client) request(req kernelRequest, res kernelResponse) error {
Expand All @@ -266,13 +223,29 @@ func (c *client) response(res kernelResponse) error {

}

func (c *client) FindObjectRef(obj reflect.Value) (refid string, ok bool) {
obj = reflect.Indirect(obj)
if obj.Kind() == reflect.Interface {
obj = reflect.ValueOf(obj.Interface()).Elem()
func (c *client) FindObjectRef(obj reflect.Value) (string, bool) {
switch obj.Kind() {
case reflect.Struct:
// Structs can be checked only if they are addressable, meaning
// they are obtained from fields of an addressable struct.
if !obj.CanAddr() {
return "", false
}
obj = obj.Addr()
fallthrough
case reflect.Interface, reflect.Ptr:
return c.objects.InstanceID(obj)
default:
// Ohter types cannot possibly be object references!
return "", false
}
}

func (c *client) GetObject(objref api.ObjectRef) interface{} {
if obj, ok := c.objects.GetObject(objref.InstanceID); ok {
return obj.Interface()
}
refid, ok = c.objects[obj]
return
panic(fmt.Errorf("no object found for ObjectRef %v", objref))
}

func (c *client) close() {
Expand Down
Loading

0 comments on commit 95cf675

Please sign in to comment.