Skip to content

Commit

Permalink
feat(go): Runtime interface type casting
Browse files Browse the repository at this point in the history
Adds an implementation map to all code generated getters and method
invocations to pass to the runtime. This allows the runtime to construct
concrete types when the return type of the function is a go interface.

Adds test for `UpcasingReflectable` which returns
`[]ReflectableEntryIface` type on the `entries` property.
  • Loading branch information
Valine committed Nov 19, 2020
1 parent ee991d4 commit bfce93d
Show file tree
Hide file tree
Showing 14 changed files with 1,658 additions and 82 deletions.
38 changes: 20 additions & 18 deletions packages/@jsii/go-runtime/jsii-calc-test/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/aws-cdk/jsii/jsii-experimental"
"math"
"os"
"strings"
"testing"
)

Expand Down Expand Up @@ -54,24 +55,25 @@ func TestCalculator(t *testing.T) {
})
}

// func TestUpcasingReflectable(t *testing.T) {
// delegate := make(map[string]interface{})
// key, val := "key1", "value1"
// delegate[key] = val
// upReflectable := calc.NewUpcasingReflectable(delegate)
// entries := upReflectable.GetEntries()
func TestUpcasingReflectable(t *testing.T) {
delegate := make(map[string]interface{})
key, val := "key1", "value1"
delegate[key] = val
upReflectable := calc.NewUpcasingReflectable(delegate)
entries := upReflectable.GetEntries()

// if len(entries) != 1 {
// t.Errorf("Entries expected to have length of: 1; Actual: %d", len(entries))
// }
if len(entries) != 1 {
t.Errorf("Entries expected to have length of: 1; Actual: %d", len(entries))
}

// entry := entries[0]
entry := entries[0]
upperKey := strings.ToUpper(key)
actualKey, actualVal := entry.GetKey(), entry.GetValue()
if actualKey != upperKey {
t.Errorf("Expected Key: %s; Received Key: %s", upperKey, actualKey)
}

// // t.Logf("Pointer entry obj: %s\n", entry)
// // t.Logf("Pointer entry addr: %p\n", entry)
// actualKey, actualVal := entry.GetKey(), entry.GetValue()
// if actualKey != key || actualVal != val {
// t.Errorf("Expected Key: %s; Received Key: %s", key, actualKey)
// t.Errorf("Expected Value: %s; Received Value: %s", val, actualVal)
// }
// }
if actualVal != val {
t.Errorf("Expected Value: %s; Received Value: %s", val, actualVal)
}
}
45 changes: 34 additions & 11 deletions packages/@jsii/go-runtime/jsii-experimental/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"regexp"
)

type implementationMap = map[reflect.Type]reflect.Type

// Load ensures a npm package is loaded in the jsii kernel.
func Load(name string, version string, tarball []byte) {
client := getClient()
Expand Down Expand Up @@ -58,7 +60,7 @@ func Create(fqn FQN, args []interface{}, interfaces []FQN, overrides []Override,

// Invoke will call a method on a jsii class instance. The response should be
// decoded into the expected return type for the method being called.
func Invoke(obj interface{}, method string, args []interface{}, returns bool, returnsPtr interface{}) {
func Invoke(obj interface{}, method string, args []interface{}, returns bool, returnsPtr interface{}, implMap implementationMap) {
client := getClient()

// Find reference to class instance in client
Expand All @@ -82,11 +84,11 @@ func Invoke(obj interface{}, method string, args []interface{}, returns bool, re
}

if returns {
castAndSetToPtr(returnsPtr, res.Result)
castAndSetToPtr(returnsPtr, res.Result, implMap)
}
}

func InvokeStatic(fqn FQN, method string, args []interface{}, returns bool, returnsPtr interface{}) {
func InvokeStatic(fqn FQN, method string, args []interface{}, returns bool, returnsPtr interface{}, implMap implementationMap) {
client := getClient()

res, err := client.sinvoke(staticInvokeRequest{
Expand All @@ -101,11 +103,11 @@ func InvokeStatic(fqn FQN, method string, args []interface{}, returns bool, retu
}

if returns {
castAndSetToPtr(returnsPtr, res.Result)
castAndSetToPtr(returnsPtr, res.Result, implMap)
}
}

func Get(obj interface{}, property string, returnsPtr interface{}) {
func Get(obj interface{}, property string, returnsPtr interface{}, implMap implementationMap) {
client := getClient()

// Find reference to class instance in client
Expand All @@ -127,10 +129,10 @@ func Get(obj interface{}, property string, returnsPtr interface{}) {
panic(err)
}

castAndSetToPtr(returnsPtr, res.Value)
castAndSetToPtr(returnsPtr, res.Value, implMap)
}

func StaticGet(fqn FQN, property string, returnsPtr interface{}) {
func StaticGet(fqn FQN, property string, returnsPtr interface{}, implMap implementationMap) {
client := getClient()

res, err := client.sget(staticGetRequest{
Expand All @@ -143,7 +145,7 @@ func StaticGet(fqn FQN, property string, returnsPtr interface{}) {
panic(err)
}

castAndSetToPtr(returnsPtr, res.Value)
castAndSetToPtr(returnsPtr, res.Value, implMap)
}

func Set(obj interface{}, property string, value interface{}) {
Expand Down Expand Up @@ -211,10 +213,31 @@ func castValToRef(data interface{}) (objref, bool) {
// argument to be the same type. Then it sets the value of the pointer element
// to be the newly casted data. This is used to cast payloads from JSII to
// expected return types for Get and Invoke functions.
func castAndSetToPtr(ptr interface{}, data interface{}) {
func castAndSetToPtr(ptr interface{}, data interface{}, implMap implementationMap) {
ptrVal := reflect.ValueOf(ptr).Elem()
val := reflect.ValueOf(data)
ptrVal.Set(val)
dataVal := reflect.ValueOf(data)

ref, isRef := castValToRef(data)

if ptrVal.Kind() == reflect.Slice && dataVal.Kind() == reflect.Slice {
// If return type is a slice, recursively cast elements
for i := 0; i < dataVal.Len(); i++ {
innerType := ptrVal.Type().Elem()
inner := reflect.New(innerType)

castAndSetToPtr(inner.Interface(), dataVal.Index(i).Interface(), implMap)
ptrVal.Set(reflect.Append(ptrVal, inner.Elem()))
}
} else if isRef {
// If return data is JSII object references, add to objects table.
concreteType := implMap[ptrVal.Type()]
ptrVal.Set(reflect.New(concreteType))
client := getClient()
client.objects[ptrVal.Interface()] = ref.JsiiInstanceId
} else {
val := reflect.ValueOf(data)
ptrVal.Set(val)
}
}

// Close finalizes the runtime process, signalling the end of the execution to
Expand Down
35 changes: 29 additions & 6 deletions packages/jsii-pacmak/lib/targets/go/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ export abstract class Package {
return findTypeInTree(this, fqn);
}

public get importPath(): string {
const moduleName = this.root.moduleName;
const prefix = moduleName !== '' ? `${moduleName}/` : '';
const rootPackageName = this.root.packageName;
const suffix = this.filePath !== '' ? `/${this.filePath}` : '';
return `${prefix}${rootPackageName}${suffix}`;
}

public emit(context: EmitContext): void {
const { code } = context;
code.openFile(this.file);
Expand Down Expand Up @@ -123,25 +131,36 @@ export abstract class Package {
);
}

protected get usesReflectionPackage(): boolean {
return (
this.types.some((type) => type.usesReflectionPackage) ||
this.submodules.some((sub) => sub.usesReflectionPackage)
);
}

private emitImports(code: CodeMaker) {
code.open('import (');
if (this.usesRuntimePackage) {
code.line(`${JSII_RT_ALIAS} "${JSII_RT_MODULE_NAME}"`);
}

if (this.usesInitPackage) {
code.line(
`${JSII_INIT_ALIAS} "${this.root.moduleName}/${this.root.packageName}/${JSII_INIT_PACKAGE}"`,
);
}

if (this.usesReflectionPackage) {
code.line(`"reflect"`);
}

for (const packageName of this.dependencyImports) {
// If the module is the same as the current one being written, don't emit an import statement
if (packageName !== this.packageName) {
code.line(`"${packageName}"`);
}
}

if (this.usesInitPackage) {
code.line(
`${JSII_INIT_ALIAS} "${this.root.moduleName}/${this.root.packageName}/${JSII_INIT_PACKAGE}"`,
);
}

code.close(')');
code.line();
}
Expand Down Expand Up @@ -188,6 +207,7 @@ export class RootPackage extends Package {
public emit(context: EmitContext): void {
super.emit(context);
this.emitJsiiPackage(context);
// this.emitTypeMapPackage(context);
this.readme?.emit(context);
}

Expand Down Expand Up @@ -245,10 +265,12 @@ export class RootPackage extends Package {
);
}
}

code.close(')');
code.line();
code.line('var once sync.Once');
code.line();

code.line(
`// ${JSII_INIT_FUNC} performs the necessary work for the enclosing`,
);
Expand All @@ -268,6 +290,7 @@ export class RootPackage extends Package {
);
code.close('})');
code.close('}');

code.closeFile(file);
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/jsii-pacmak/lib/targets/go/runtime/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const JSII_INIT_PACKAGE = 'jsii';
export const JSII_INIT_FUNC = 'Initialize';
// Alias used for the jsii init
export const JSII_INIT_ALIAS = '_init_';
// Type of global implementation map
export const JSII_IMPL_MAP_TYPE = `map[reflect.Type]reflect.Type`;

// Function to make create request
export const JSII_CREATE_FUNC = `${JSII_RT_ALIAS}.Create`;
Expand Down
17 changes: 16 additions & 1 deletion packages/jsii-pacmak/lib/targets/go/runtime/method-call.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { CodeMaker } from 'codemaker';

import { GoMethod } from '../types';
import { JSII_INVOKE_FUNC, JSII_SINVOKE_FUNC } from './constants';
import {
JSII_INVOKE_FUNC,
JSII_SINVOKE_FUNC,
JSII_IMPL_MAP_TYPE,
} from './constants';
import { slugify, emitInitialization } from './util';

export class MethodCall {
Expand All @@ -17,6 +21,7 @@ export class MethodCall {

private emitDynamic(code: CodeMaker) {
code.line(`var ${this.returnVarName} ${this.concreteReturnType}`);
code.line(`${this.implMapVar} := make(${JSII_IMPL_MAP_TYPE})`);
code.open(`${JSII_INVOKE_FUNC}(`);

const returnsArg = this.parent.returnsRef
Expand All @@ -28,6 +33,7 @@ export class MethodCall {
code.line(`${this.argsString},`);
code.line(`${this.returnsVal ? 'true' : 'false'},`);
code.line(`${returnsArg},`);
code.line(`${this.implMapVar},`);

code.close(`)`);

Expand All @@ -39,6 +45,7 @@ export class MethodCall {
private emitStatic(code: CodeMaker) {
emitInitialization(code);
code.line(`var ${this.returnVarName} ${this.concreteReturnType}`);
code.line(`${this.implMapVar} := make(${JSII_IMPL_MAP_TYPE})`);

code.open(`${JSII_SINVOKE_FUNC}(`);

Expand All @@ -47,6 +54,7 @@ export class MethodCall {
code.line(`${this.argsString},`);
code.line(`${this.returnsVal ? 'true' : 'false'},`);
code.line(`&${this.returnVarName},`);
code.line(`${this.implMapVar},`);

code.close(`)`);

Expand All @@ -62,6 +70,13 @@ export class MethodCall {
);
}

private get implMapVar(): string {
return slugify(
'implMap',
this.parent.parameters.map((p) => p.name),
);
}

private get returnsVal(): boolean {
return Boolean(this.parent.reference && !this.parent.reference.void);
}
Expand Down
25 changes: 25 additions & 0 deletions packages/jsii-pacmak/lib/targets/go/runtime/property-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
JSII_SET_FUNC,
JSII_SGET_FUNC,
JSII_SSET_FUNC,
JSII_IMPL_MAP_TYPE,
} from './constants';
import { slugify, emitInitialization } from './util';

Expand All @@ -14,12 +15,24 @@ export class GetProperty {

public emit(code: CodeMaker) {
const resultVar = slugify('returns', [this.parent.instanceArg]);
const implMapVar = slugify('implMap', [this.parent.instanceArg]);
code.line(`var ${resultVar} ${this.parent.returnType}`);
code.line(`${implMapVar} := make(${JSII_IMPL_MAP_TYPE})`);

const implMap =
this.parent.reference?.scopedImplMap(this.parent.parent.pkg) ?? [];
if (implMap.length) {
const [interfaceName, structName] = implMap;
code.line(
`${implMapVar}[reflect.TypeOf((*${interfaceName})(nil)).Elem()] = reflect.TypeOf((*${structName})(nil)).Elem()`,
);
}

code.open(`${JSII_GET_FUNC}(`);
code.line(`${this.parent.instanceArg},`);
code.line(`"${this.parent.property.name}",`);
code.line(`&${resultVar},`);
code.line(`${implMapVar},`);
code.close(`)`);

code.line(`return ${resultVar}`);
Expand All @@ -44,12 +57,24 @@ export class StaticGetProperty {
public emit(code: CodeMaker) {
emitInitialization(code);
const resultVar = slugify('returns', []);
const implMapVar = slugify('implMap', [this.parent.instanceArg]);
code.line(`var ${resultVar} ${this.parent.returnType}`);
code.line(`${implMapVar} := make(${JSII_IMPL_MAP_TYPE})`);

const implMap =
this.parent.reference?.scopedImplMap(this.parent.parent.pkg) ?? [];
if (implMap.length) {
const [interfaceName, structName] = implMap;
code.line(
`${implMapVar}[reflect.TypeOf((*${interfaceName})(nil)).Elem()] = reflect.TypeOf((*${structName})(nil)).Elem()`,
);
}

code.open(`${JSII_SGET_FUNC}(`);
code.line(`"${this.parent.parent.fqn}",`);
code.line(`"${this.parent.property.name}",`);
code.line(`&${resultVar},`);
code.line(`${implMapVar},`);
code.close(`)`);

code.line(`return ${resultVar}`);
Expand Down
11 changes: 11 additions & 0 deletions packages/jsii-pacmak/lib/targets/go/types/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ export class GoClass extends GoStruct {
);
}

public get usesReflectionPackage() {
return (
this.properties.some((p) => p.usesReflectionPackage) ||
this.methods.some((m) => m.usesReflectionPackage)
);
}

protected emitInterface(context: EmitContext): void {
const { code } = context;
code.line('// Class interface'); // FIXME for debugging
Expand Down Expand Up @@ -241,6 +248,10 @@ export class ClassMethod extends GoMethod {
public get instanceArg(): string {
return this.parent.name.substring(0, 1).toLowerCase();
}

public get usesReflectionPackage() {
return Boolean(this.reference?.scopedImplMap(this.parent.pkg).length);
}
}

export class StaticMethod extends ClassMethod {
Expand Down
Loading

0 comments on commit bfce93d

Please sign in to comment.