diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fab3dfd957..f6f070a856 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,10 +31,10 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.x' - - name: Set up Go 1.16 + - name: Set up Go 1.17 uses: actions/setup-go@v2 with: - go-version: '1.16' + go-version: '1.17' - name: Set up Java 8 uses: actions/setup-java@v2 with: @@ -125,10 +125,10 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.x' - - name: Set up Go 1.16 + - name: Set up Go 1.17 uses: actions/setup-go@v2 with: - go-version: '1.16' + go-version: '1.17' - name: Set up Java 8 uses: actions/setup-java@v2 with: @@ -211,7 +211,7 @@ jobs: matrix: # All currently supported node versions (Maintenance LTS, Active LTS, Current) dotnet: ['3.1.x'] - go: ['1.16'] + go: ['1.17'] java: ['8'] node: ['12', '14', '16'] os: [ubuntu-latest] @@ -221,53 +221,53 @@ jobs: # Test using Windows - os: windows-latest dotnet: '3.1.x' - go: '1.16' + go: '1.17' java: '8' node: '12' python: '3.6' # Test using macOS - os: macos-latest dotnet: '3.1.x' - go: '1.16' + go: '1.17' java: '8' node: '12' python: '3.6' # Test alternate .NETs - java: '8' dotnet: '5.0.x' - go: '1.16' + go: '1.17' node: '12' os: ubuntu-latest python: '3.6' - java: '8' dotnet: '6.0.x' - go: '1.16' + go: '1.17' node: '12' os: ubuntu-latest python: '3.6' # Test alternate Javas - java: '11' dotnet: '3.1.x' - go: '1.16' + go: '1.17' node: '12' os: ubuntu-latest python: '3.6' # Test alternate Pythons - python: '3.7' dotnet: '3.1.x' - go: '1.16' + go: '1.17' java: '8' node: '12' os: ubuntu-latest - python: '3.8' dotnet: '3.1.x' - go: '1.16' + go: '1.17' java: '8' node: '12' os: ubuntu-latest - python: '3.9' dotnet: '3.1.x' - go: '1.16' + go: '1.17' java: '8' node: '12' os: ubuntu-latest diff --git a/gh-pages/content/specification/6-compliance-report.md b/gh-pages/content/specification/6-compliance-report.md index 82aec9f08e..b68e816b40 100644 --- a/gh-pages/content/specification/6-compliance-report.md +++ b/gh-pages/content/specification/6-compliance-report.md @@ -5,7 +5,7 @@ This section details the current state of each language binding with respect to our standard compliance suite. -| number | test | java (99.16%) | golang (78.15%) | Dotnet | Python | +| number | test | java (98.33%) | golang (78.33%) | Dotnet | Python | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------------------------------------------- | ------ | ------ | | 1 | asyncOverrides_overrideCallsSuper | 🟢 | [🔴](https://github.com/aws/jsii/issues/2670) | â­• | â­• | | 2 | [arrayReturnedByMethodCanBeRead]("Array created in the kernel can be queried for its elements") | 🟢 | 🟢 | â­• | â­• | @@ -126,3 +126,4 @@ This section details the current state of each language binding with respect to | 117 | testInterfaces | 🟢 | 🟢 | â­• | â­• | | 118 | [callbackParameterIsInterface]("Validates pure interfaces can be passed to callbacks") | â­• | 🟢 | â­• | â­• | | 119 | [classCanBeUsedWhenNotExpressedlyLoaded]("Validates that types not explicitly loaded by the user can safely be returned by JS code") | 🟢 | 🟢 | â­• | â­• | +| 120 | [downcasting]("Ensures unsafe-cast features work as expected") | â­• | 🟢 | â­• | â­• | diff --git a/packages/@jsii/go-runtime-test/project/compliance_test.go b/packages/@jsii/go-runtime-test/project/compliance_test.go index bc2f1c1247..afff67374c 100644 --- a/packages/@jsii/go-runtime-test/project/compliance_test.go +++ b/packages/@jsii/go-runtime-test/project/compliance_test.go @@ -1645,6 +1645,16 @@ func (suite *ComplianceSuite) TestClassCanBeUsedWhenNotExpressedlyLoaded() { cdk16625.New().Test() } +func (suite *ComplianceSuite) TestDownCasting() { + require := suite.Require() + + anyValue := calc.SomeTypeJsii976_ReturnAnonymous() + var realValue calc.IReturnJsii976 + + jsii.UnsafeCast(anyValue, &realValue) + + require.Equal(realValue.Foo(), jsii.Number(1337)) +} // required to make `go test` recognize the suite. func TestComplianceSuite(t *testing.T) { diff --git a/packages/@jsii/go-runtime-test/project/go.mod b/packages/@jsii/go-runtime-test/project/go.mod index a71db58144..739b059c53 100644 --- a/packages/@jsii/go-runtime-test/project/go.mod +++ b/packages/@jsii/go-runtime-test/project/go.mod @@ -1,6 +1,6 @@ module github.com/aws/jsii/go-runtime-test -go 1.15 +go 1.17 require ( github.com/aws/jsii-runtime-go v0.0.0 @@ -12,6 +12,17 @@ require ( golang.org/x/tools v0.1.0 ) +require ( + github.com/Masterminds/semver/v3 v3.1.1 // indirect + github.com/aws/jsii/jsii-calc/go/scopejsiicalcbaseofbase/v2 v2.1.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/mod v0.3.0 // indirect + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) + replace ( github.com/aws/jsii-runtime-go => ../../go-runtime/jsii-runtime-go github.com/aws/jsii/jsii-calc/go/jcb => ../jsii-calc/go/jcb diff --git a/packages/@jsii/go-runtime-test/project/go.sum b/packages/@jsii/go-runtime-test/project/go.sum index ceb0e37170..57ec351287 100644 --- a/packages/@jsii/go-runtime-test/project/go.sum +++ b/packages/@jsii/go-runtime-test/project/go.sum @@ -1,7 +1,8 @@ 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/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -39,5 +40,6 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/packages/@jsii/go-runtime-test/project/internal/cdk16625/cdk16625.go b/packages/@jsii/go-runtime-test/project/internal/cdk16625/cdk16625.go index aef3945a2a..05452a537d 100644 --- a/packages/@jsii/go-runtime-test/project/internal/cdk16625/cdk16625.go +++ b/packages/@jsii/go-runtime-test/project/internal/cdk16625/cdk16625.go @@ -11,7 +11,7 @@ func New() abc.Cdk16625 { return c } -type cdk16625 struct{ +type cdk16625 struct { abc.Cdk16625 } diff --git a/packages/@jsii/go-runtime-test/project/tools.go b/packages/@jsii/go-runtime-test/project/tools.go index c8bdab1fff..73d81778ef 100644 --- a/packages/@jsii/go-runtime-test/project/tools.go +++ b/packages/@jsii/go-runtime-test/project/tools.go @@ -1,3 +1,4 @@ +//go:build tools // +build tools // Package tools contains the necessary statements to ensure tool dependencies diff --git a/packages/@jsii/go-runtime/jsii-runtime-go/cast.go b/packages/@jsii/go-runtime/jsii-runtime-go/cast.go new file mode 100644 index 0000000000..935dc793e6 --- /dev/null +++ b/packages/@jsii/go-runtime/jsii-runtime-go/cast.go @@ -0,0 +1,59 @@ +package jsii + +import ( + "fmt" + "reflect" + + "github.com/aws/jsii-runtime-go/internal/kernel" +) + +// UnsafeCast converts the given interface value to the desired target interface +// pointer. Panics if the from value is not a jsii proxy object, or if the to +// value is not a pointer to an interface type. +func UnsafeCast(from interface{}, into interface{}) { + rinto := reflect.ValueOf(into) + if rinto.Kind() != reflect.Ptr { + panic(fmt.Errorf("Second argument to UnsafeCast must be a pointer to an interface. Received %s", rinto.Type().String())) + } + rinto = rinto.Elem() + if rinto.Kind() != reflect.Interface { + panic(fmt.Errorf("Second argument to UnsafeCast must be a pointer to an interface. Received pointer to %s", rinto.Type().String())) + } + + rfrom := reflect.ValueOf(from) + + // If rfrom is essentially nil, set into to nil and return. + if !rfrom.IsValid() || rfrom.IsZero() { + null := reflect.Zero(rinto.Type()) + rinto.Set(null) + return + } + // Interfaces may present as a pointer to an implementing struct, and that's fine... + if rfrom.Kind() != reflect.Interface && rfrom.Kind() != reflect.Ptr { + panic(fmt.Errorf("First argument to UnsafeCast must be an interface value. Received %s", rfrom.Type().String())) + } + + // If rfrom can be directly converted to rinto, just do it. + if rfrom.CanConvert(rinto.Type()) { + rfrom = rfrom.Convert(rinto.Type()) + rinto.Set(rfrom) + return + } + + client := kernel.GetClient() + if objID, found := client.FindObjectRef(rfrom); found { + // Ensures the value is initialized properly. Panics if the target value is not a jsii interface type. + client.Types().InitJsiiProxy(rinto) + + // If the target type is a behavioral interface, add it to the ObjectRef.Interfaces list. + if fqn, found := client.Types().InterfaceFQN(rinto.Type()); found { + objID.Interfaces = append(objID.Interfaces, fqn) + } + + // Make the new value an alias to the old value. + client.RegisterInstance(rinto, objID) + return + } + + panic(fmt.Errorf("First argument to UnsafeCast must be a jsii proxy value. Received %s", rfrom.String())) +} diff --git a/packages/@jsii/go-runtime/jsii-runtime-go/cast_test.go b/packages/@jsii/go-runtime/jsii-runtime-go/cast_test.go new file mode 100644 index 0000000000..09ff1ffeef --- /dev/null +++ b/packages/@jsii/go-runtime/jsii-runtime-go/cast_test.go @@ -0,0 +1,101 @@ +package jsii + +import ( + "reflect" + "testing" + + "github.com/aws/jsii-runtime-go/internal/api" + "github.com/aws/jsii-runtime-go/internal/kernel" +) + +type MockInterfaceABase interface { + MockMethodABase(_ float64) +} + +type mockABase struct { + _ int // padding +} + +func (m *mockABase) MockMethodABase(_ float64) {} + +type MockInterfaceA interface { + MockInterfaceABase + MockMethodA(_ string) +} + +func NewMockInterfaceA() MockInterfaceA { + return &mockA{mockABase{}} +} + +type mockA struct { + mockABase +} + +func (m *mockA) MockMethodA(_ string) {} + +type MockInterfaceB interface { + MockMethodB(_ int) +} + +func NewMockInterfaceB() MockInterfaceB { + return &mockB{} +} + +type mockB struct { + _ int // Padding +} + +func (m *mockB) MockMethodB(_ int) {} + +func TestNilSource(t *testing.T) { + // Make "into" not nil to ensure the cast function overwrites it. + into := NewMockInterfaceB() + UnsafeCast(nil, &into) + + if into != nil { + t.Fail() + } +} + +func TestSourceAndTargetAreTheSame(t *testing.T) { + into := NewMockInterfaceB() + original := into + UnsafeCast(into, &into) + + if into != original { + t.Fail() + } +} + +func TestTargetIsSubclassOfSource(t *testing.T) { + from := NewMockInterfaceA() + var into MockInterfaceABase + UnsafeCast(from, &into) + + if into != from { + t.Fail() + } +} + +func TestRegistersAlias(t *testing.T) { + client := kernel.GetClient() + + objid := api.ObjectRef{InstanceID: "Object@1337#42"} + from := NewMockInterfaceA() + client.RegisterInstance(reflect.ValueOf(from), objid) + + var into MockInterfaceB + client.Types().RegisterInterface(api.FQN("mock.InterfaceB"), reflect.TypeOf(&into).Elem(), []api.Override{}, func() interface{} { return NewMockInterfaceB() }) + + UnsafeCast(from, &into) + + if into == nil { + t.Fail() + } + + if refid, found := client.FindObjectRef(reflect.ValueOf(into)); !found { + t.Fail() + } else if refid.InstanceID != objid.InstanceID { + t.Fail() + } +} diff --git a/packages/@jsii/go-runtime/jsii-runtime-go/go.mod b/packages/@jsii/go-runtime/jsii-runtime-go/go.mod index 4545261637..d72b03252a 100644 --- a/packages/@jsii/go-runtime/jsii-runtime-go/go.mod +++ b/packages/@jsii/go-runtime/jsii-runtime-go/go.mod @@ -1,10 +1,16 @@ module github.com/aws/jsii-runtime-go -go 1.16 +go 1.17 require ( github.com/Masterminds/semver/v3 v3.1.1 github.com/stretchr/testify v1.7.0 ) +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) + retract v1.27.0 diff --git a/packages/@jsii/go-runtime/jsii-runtime-go/go.sum b/packages/@jsii/go-runtime/jsii-runtime-go/go.sum index 46621f39b3..34dd278f55 100644 --- a/packages/@jsii/go-runtime/jsii-runtime-go/go.sum +++ b/packages/@jsii/go-runtime/jsii-runtime-go/go.sum @@ -1,14 +1,15 @@ 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/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/client.go b/packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/client.go index d0f4eeeec7..944e420644 100644 --- a/packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/client.go +++ b/packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/client.go @@ -95,29 +95,35 @@ func (c *Client) Types() *typeregistry.TypeRegistry { return types } -func (c *Client) RegisterInstance(instance reflect.Value, instanceID string) error { - return c.objects.Register(instance, instanceID) +func (c *Client) RegisterInstance(instance reflect.Value, objectRef api.ObjectRef) error { + return c.objects.Register(instance, objectRef) } func (c *Client) request(req kernelRequester, res kernelResponder) error { return c.process.Request(req, res) } -func (c *Client) FindObjectRef(obj reflect.Value) (string, bool) { +func (c *Client) FindObjectRef(obj reflect.Value) (ref api.ObjectRef, found bool) { + ref = api.ObjectRef{} + found = false + 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 + return } obj = obj.Addr() fallthrough case reflect.Interface, reflect.Ptr: - return c.objects.InstanceID(obj) + if ref.InstanceID, found = c.objects.InstanceID(obj); found { + ref.Interfaces = c.objects.Interfaces(ref.InstanceID) + } + return default: // Other types cannot possibly be object references! - return "", false + return } } diff --git a/packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/conversions.go b/packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/conversions.go index 97be302f36..16896047f4 100644 --- a/packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/conversions.go +++ b/packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/conversions.go @@ -71,7 +71,7 @@ func (c *Client) castAndSetToPtr(ptr reflect.Value, data reflect.Value) { // If return data is jsii object references, add to objects table. if err := c.Types().InitJsiiProxy(ptr); err == nil { - if err = c.RegisterInstance(ptr, ref.InstanceID); err != nil { + if err = c.RegisterInstance(ptr, ref); err != nil { panic(err) } } else { @@ -157,7 +157,7 @@ func (c *Client) CastPtrToRef(dataVal reflect.Value) interface{} { case reflect.Interface, reflect.Ptr: if valref, valHasRef := c.FindObjectRef(dataVal); valHasRef { - return api.ObjectRef{InstanceID: valref} + return valref } // In case we got a pointer to a map, slice, enum, ... diff --git a/packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/manage-object.go b/packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/manage-object.go index c755c83cb1..7c0e6416ab 100644 --- a/packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/manage-object.go +++ b/packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/manage-object.go @@ -26,7 +26,7 @@ func (c *Client) ManageObject(v reflect.Value) (ref api.ObjectRef, err error) { }) if err == nil { - if err = c.objects.Register(v, resp.InstanceID); err == nil { + if err = c.objects.Register(v, api.ObjectRef{InstanceID: resp.InstanceID, Interfaces: interfaces}); err == nil { ref.InstanceID = resp.InstanceID } } diff --git a/packages/@jsii/go-runtime/jsii-runtime-go/internal/objectstore/objectstore.go b/packages/@jsii/go-runtime/jsii-runtime-go/internal/objectstore/objectstore.go index 6944056a7f..f15828ac67 100644 --- a/packages/@jsii/go-runtime/jsii-runtime-go/internal/objectstore/objectstore.go +++ b/packages/@jsii/go-runtime/jsii-runtime-go/internal/objectstore/objectstore.go @@ -3,8 +3,14 @@ package objectstore import ( "fmt" "reflect" + + "github.com/aws/jsii-runtime-go/internal/api" ) +// stringSet is a set of strings, implemented as a map from string to an +// arbitrary 0-width value. +type stringSet map[string]struct{} + // ObjectStore tracks object instances for which an identifier has been // associated. Object to instanceID association is tracked using the object // memory address (aka pointer value) in order to not have issues with go's @@ -20,13 +26,21 @@ type ObjectStore struct { // represents the top-level object that was registered with the instanceID // via the Register method. idToObject map[string]reflect.Value + + // idToInterfaces associates an instanceID with the set of interfaces that it + // is known to implement. + // + // Incorrect use of the UnsafeCast function may result in an instance's + // interface list containing interfaces that it does not actually implement. + idToInterfaces map[string]stringSet } // New initializes a new ObjectStore. func New() *ObjectStore { return &ObjectStore{ - objectToID: make(map[uintptr]string), - idToObject: make(map[string]reflect.Value), + objectToID: make(map[uintptr]string), + idToObject: make(map[string]reflect.Value), + idToInterfaces: make(map[string]stringSet), } } @@ -42,7 +56,7 @@ func New() *ObjectStore { // // The call is idempotent: calling Register again with the same value and // instanceID does not result in an error. -func (o *ObjectStore) Register(value reflect.Value, instanceID string) error { +func (o *ObjectStore) Register(value reflect.Value, objectRef api.ObjectRef) error { var err error if value, err = canonicalValue(value); err != nil { return err @@ -50,16 +64,18 @@ func (o *ObjectStore) Register(value reflect.Value, instanceID string) error { ptr := value.Pointer() if existing, found := o.objectToID[ptr]; found { - if existing == instanceID { + if existing == objectRef.InstanceID { + o.mergeInterfaces(objectRef) return nil } - return fmt.Errorf("attempting to register %s as %s, but it was already registered as %s", value, instanceID, existing) + return fmt.Errorf("attempting to register %s as %s, but it was already registered as %s", value, objectRef.InstanceID, existing) } aliases := findAliases(value) - if existing, found := o.idToObject[instanceID]; found { + if existing, found := o.idToObject[objectRef.InstanceID]; found { if existing == value { + o.mergeInterfaces(objectRef) return nil } // Value already exists (e.g: a constructor made a callback with "this" @@ -70,20 +86,46 @@ func (o *ObjectStore) Register(value reflect.Value, instanceID string) error { for _, alias := range aliases { ptr := alias.Pointer() - if existing, found := o.objectToID[ptr]; found && existing != instanceID { - return fmt.Errorf("value %s is embedded in %s which has ID %s, but was already assigned %s", alias.String(), value.String(), instanceID, existing) + if existing, found := o.objectToID[ptr]; found && existing != objectRef.InstanceID { + return fmt.Errorf("value %s is embedded in %s which has ID %s, but was already assigned %s", alias.String(), value.String(), objectRef.InstanceID, existing) } } - o.objectToID[ptr] = instanceID - o.idToObject[instanceID] = value + o.objectToID[ptr] = objectRef.InstanceID + o.idToObject[objectRef.InstanceID] = value for _, alias := range aliases { - o.objectToID[alias.Pointer()] = instanceID + o.objectToID[alias.Pointer()] = objectRef.InstanceID } + o.mergeInterfaces(objectRef) + return nil } +// mergeInterfaces adds all interfaces carried by the provided objectRef to the +// tracking set for the objectRef's InstanceID. Does nothing if no interfaces +// are designated on the objectRef. +func (o *ObjectStore) mergeInterfaces(objectRef api.ObjectRef) { + // If we don't have interfaces, we have nothing to do... + if objectRef.Interfaces == nil { + return + } + + // Find or create the interface list for the relevant InstanceID + var interfaces stringSet + if list, found := o.idToInterfaces[objectRef.InstanceID]; found { + interfaces = list + } else { + interfaces = make(stringSet) + o.idToInterfaces[objectRef.InstanceID] = interfaces + } + + // Add any missing interface to the list. + for _, iface := range objectRef.Interfaces { + interfaces[string(iface)] = struct{}{} + } +} + // InstanceID attempts to determine the instanceID associated with the provided // value, if any. Returns the existing instanceID and a boolean informing // whether an instanceID was already found or not. @@ -99,6 +141,23 @@ func (o *ObjectStore) InstanceID(value reflect.Value) (instanceID string, found return } +// Interfaces returns the set of interfaces associated with the provided +// instanceID. +// +// It returns a nil slice in case the instancceID is invalid, or if it does not +// have any associated interfaces. +func (o *ObjectStore) Interfaces(instanceID string) []api.FQN { + if set, found := o.idToInterfaces[instanceID]; found { + interfaces := make([]api.FQN, 0, len(set)) + for iface := range set { + interfaces = append(interfaces, api.FQN(iface)) + } + return interfaces + } else { + return nil + } +} + // GetObject attempts to retrieve the object value associated with the given // instanceID. Returns the existing value and a boolean informing whether a // value was associated with this instanceID or not. diff --git a/packages/@jsii/go-runtime/jsii-runtime-go/internal/typeregistry/registration.go b/packages/@jsii/go-runtime/jsii-runtime-go/internal/typeregistry/registration.go index aab2ab3d9b..c1245762ca 100644 --- a/packages/@jsii/go-runtime/jsii-runtime-go/internal/typeregistry/registration.go +++ b/packages/@jsii/go-runtime/jsii-runtime-go/internal/typeregistry/registration.go @@ -96,7 +96,12 @@ func (t *TypeRegistry) RegisterInterface(fqn api.FQN, iface reflect.Type, overri return fmt.Errorf("another type was already registered with %s: %v", fqn, existing) } + if existing, exists := t.typeToInterfaceFQN[iface]; exists && existing != fqn { + return fmt.Errorf("anoter FQN was already registered with %v: %s", iface, existing) + } + t.fqnToType[fqn] = registeredType{iface, interfaceType} + t.typeToInterfaceFQN[iface] = fqn t.proxyMakers[iface] = maker // Skipping registration if there are no members, as this would have no use. diff --git a/packages/@jsii/go-runtime/jsii-runtime-go/internal/typeregistry/typeregistry.go b/packages/@jsii/go-runtime/jsii-runtime-go/internal/typeregistry/typeregistry.go index 50d969e25d..2f1bb1d641 100644 --- a/packages/@jsii/go-runtime/jsii-runtime-go/internal/typeregistry/typeregistry.go +++ b/packages/@jsii/go-runtime/jsii-runtime-go/internal/typeregistry/typeregistry.go @@ -26,6 +26,10 @@ type TypeRegistry struct { // enum FQN (e.g. "jsii-calc.StringEnum") typeToEnumFQN map[reflect.Type]api.FQN + // typeToInterfaceFQN maps Go interface type ("SomeInterface") to the + // corresponding jsii interface FQN (e.g: "jsii-calc.SomeInterface") + typeToInterfaceFQN map[reflect.Type]api.FQN + // structInfo maps registered struct types to all their fields. structInfo map[reflect.Type]registeredStruct @@ -39,14 +43,23 @@ type TypeRegistry struct { // New creates a new type registry. func New() *TypeRegistry { - return &TypeRegistry{ - fqnToType: make(map[api.FQN]registeredType), - fqnToEnumMember: make(map[string]interface{}), - typeToEnumFQN: make(map[reflect.Type]api.FQN), - structInfo: make(map[reflect.Type]registeredStruct), - proxyMakers: make(map[reflect.Type]func() interface{}), - typeMembers: make(map[api.FQN][]api.Override), + registry := TypeRegistry{ + fqnToType: make(map[api.FQN]registeredType), + fqnToEnumMember: make(map[string]interface{}), + typeToEnumFQN: make(map[reflect.Type]api.FQN), + typeToInterfaceFQN: make(map[reflect.Type]api.FQN), + structInfo: make(map[reflect.Type]registeredStruct), + proxyMakers: make(map[reflect.Type]func() interface{}), + typeMembers: make(map[api.FQN][]api.Override), + } + + // Ensure we can initialize proxies for `interface{}` when a method returns `any`. + registry.proxyMakers[reflect.TypeOf((*interface{})(nil)).Elem()] = func() interface{} { + type object struct{ _ int } // Padded so it's not 0-sized + return &object{} } + + return ®istry } // StructFields returns the list of fields associated with a jsii struct type, @@ -146,3 +159,8 @@ func (t *TypeRegistry) TryRenderEnumRef(value reflect.Value) (ref *api.EnumRef, return } + +func (t *TypeRegistry) InterfaceFQN(typ reflect.Type) (fqn api.FQN, found bool) { + fqn, found = t.typeToInterfaceFQN[typ] + return +} diff --git a/packages/@jsii/go-runtime/jsii-runtime-go/runtime/runtime.go b/packages/@jsii/go-runtime/jsii-runtime-go/runtime/runtime.go index 94ef2c2ba2..d9bde090b8 100644 --- a/packages/@jsii/go-runtime/jsii-runtime-go/runtime/runtime.go +++ b/packages/@jsii/go-runtime/jsii-runtime-go/runtime/runtime.go @@ -2,10 +2,11 @@ package runtime import ( "fmt" - "github.com/aws/jsii-runtime-go/internal/api" - "github.com/aws/jsii-runtime-go/internal/kernel" "reflect" "strings" + + "github.com/aws/jsii-runtime-go/internal/api" + "github.com/aws/jsii-runtime-go/internal/kernel" ) // FQN represents a fully-qualified type name in the jsii type system. @@ -185,7 +186,7 @@ func Create(fqn FQN, args []interface{}, inst interface{}) { panic(err) } - if err = client.RegisterInstance(instVal, res.InstanceID); err != nil { + if err = client.RegisterInstance(instVal, api.ObjectRef{InstanceID: res.InstanceID, Interfaces: interfaces}); err != nil { panic(err) } } @@ -196,7 +197,7 @@ func Invoke(obj interface{}, method string, args []interface{}, ret interface{}) client := kernel.GetClient() // Find reference to class instance in client - refid, found := client.FindObjectRef(reflect.ValueOf(obj)) + ref, found := client.FindObjectRef(reflect.ValueOf(obj)) if !found { panic("No Object Found") @@ -205,9 +206,7 @@ func Invoke(obj interface{}, method string, args []interface{}, ret interface{}) res, err := client.Invoke(kernel.InvokeProps{ Method: method, Arguments: convertArguments(args), - ObjRef: api.ObjectRef{ - InstanceID: refid, - }, + ObjRef: ref, }) if err != nil { @@ -222,7 +221,7 @@ func InvokeVoid(obj interface{}, method string, args []interface{}) { client := kernel.GetClient() // Find reference to class instance in client - refid, found := client.FindObjectRef(reflect.ValueOf(obj)) + ref, found := client.FindObjectRef(reflect.ValueOf(obj)) if !found { panic("No Object Found") @@ -231,7 +230,7 @@ func InvokeVoid(obj interface{}, method string, args []interface{}) { _, err := client.Invoke(kernel.InvokeProps{ Method: method, Arguments: convertArguments(args), - ObjRef: api.ObjectRef{InstanceID: refid}, + ObjRef: ref, }) if err != nil { @@ -278,7 +277,7 @@ func Get(obj interface{}, property string, ret interface{}) { client := kernel.GetClient() // Find reference to class instance in client - refid, found := client.FindObjectRef(reflect.ValueOf(obj)) + ref, found := client.FindObjectRef(reflect.ValueOf(obj)) if !found { panic(fmt.Errorf("no object reference found for %v", obj)) @@ -286,9 +285,7 @@ func Get(obj interface{}, property string, ret interface{}) { res, err := client.Get(kernel.GetProps{ Property: property, - ObjRef: api.ObjectRef{ - InstanceID: refid, - }, + ObjRef: ref, }) if err != nil { @@ -321,7 +318,7 @@ func Set(obj interface{}, property string, value interface{}) { client := kernel.GetClient() // Find reference to class instance in client - refid, found := client.FindObjectRef(reflect.ValueOf(obj)) + ref, found := client.FindObjectRef(reflect.ValueOf(obj)) if !found { panic("No Object Found") @@ -330,9 +327,7 @@ func Set(obj interface{}, property string, value interface{}) { _, err := client.Set(kernel.SetProps{ Property: property, Value: client.CastPtrToRef(reflect.ValueOf(value)), - ObjRef: api.ObjectRef{ - InstanceID: refid, - }, + ObjRef: ref, }) if err != nil { diff --git a/packages/@jsii/kernel/lib/api.ts b/packages/@jsii/kernel/lib/api.ts index 44b9cb488a..b339a4716b 100644 --- a/packages/@jsii/kernel/lib/api.ts +++ b/packages/@jsii/kernel/lib/api.ts @@ -7,9 +7,6 @@ export const TOKEN_STRUCT = '$jsii.struct'; export interface ObjRef { readonly [TOKEN_REF]: string; -} - -export interface AnnotatedObjRef extends ObjRef { [TOKEN_INTERFACES]?: readonly string[]; } @@ -138,8 +135,7 @@ export interface CreateRequest { readonly overrides?: Override[]; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface CreateResponse extends AnnotatedObjRef {} +export type CreateResponse = ObjRef; export interface DelRequest { readonly objref: ObjRef; diff --git a/packages/@jsii/kernel/lib/kernel.ts b/packages/@jsii/kernel/lib/kernel.ts index d6f5f220d8..de6518ec5b 100644 --- a/packages/@jsii/kernel/lib/kernel.ts +++ b/packages/@jsii/kernel/lib/kernel.ts @@ -965,7 +965,7 @@ export class Kernel { interfaces: string[] = [], ): spec.Method | undefined { for (const fqn of [classFqn, ...interfaces]) { - if (fqn === 'Object') { + if (fqn === wire.EMPTY_OBJECT_FQN) { continue; } const typeinfo = this._typeInfoForFqn(fqn); diff --git a/packages/@jsii/kernel/lib/objects.ts b/packages/@jsii/kernel/lib/objects.ts index 305d93ccb3..79eec58cae 100644 --- a/packages/@jsii/kernel/lib/objects.ts +++ b/packages/@jsii/kernel/lib/objects.ts @@ -34,7 +34,7 @@ export function jsiiTypeFqn(obj: any): string | undefined { * * This is to retain object identity across invocations. */ -export function objectReference(obj: unknown): api.AnnotatedObjRef | undefined { +export function objectReference(obj: unknown): api.ObjRef | undefined { // If this object as already returned if ((obj as any)[OBJID_SYMBOL]) { return { @@ -92,7 +92,7 @@ export class ObjectTable { obj: unknown, fqn: string, interfaces?: string[], - ): api.AnnotatedObjRef { + ): api.ObjRef { if (fqn === undefined) { throw new Error('FQN cannot be undefined'); } @@ -135,6 +135,26 @@ export class ObjectTable { if (!obj) { throw new Error(`Object ${objid} not found`); } + + // If there are "additional" interfaces declared on the objref, merge them + // into the returned object. This is used to support client-side forced + // down-casting (a.k.a: unsafe casting). We do NOT register the extra + // interfaces here so that if the client provided an interface that is + // actually not implemented, we aren't "poisoning" our state with that + // incorrect information. + const additionalInterfaces = objref[api.TOKEN_INTERFACES]; + if (additionalInterfaces != null && additionalInterfaces.length > 0) { + return { + ...obj, + interfaces: [ + ...(obj.interfaces ?? []), + // We append at the end so "registered" interface information has + // precedence over client-declared ones. + ...additionalInterfaces, + ], + }; + } + return obj; } diff --git a/packages/@jsii/kernel/lib/serialization.ts b/packages/@jsii/kernel/lib/serialization.ts index c1295613d6..caec15fdc0 100644 --- a/packages/@jsii/kernel/lib/serialization.ts +++ b/packages/@jsii/kernel/lib/serialization.ts @@ -420,7 +420,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { */ host.debug('Returning value type by reference'); - return host.objects.registerObject(value, 'Object', [ + return host.objects.registerObject(value, EMPTY_OBJECT_FQN, [ (optionalValue.type as spec.NamedTypeReference).fqn, ]); }, @@ -520,7 +520,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { : undefined; const jsiiType = jsiiTypeFqn(value) ?? - (spec.isClassType(expectedType) ? expectedType.fqn : 'Object'); + (spec.isClassType(expectedType) ? expectedType.fqn : EMPTY_OBJECT_FQN); return host.objects.registerObject(value, jsiiType, interfaces); }, diff --git a/superchain/Dockerfile b/superchain/Dockerfile index 25525320ca..be8abe0eff 100644 --- a/superchain/Dockerfile +++ b/superchain/Dockerfile @@ -66,7 +66,7 @@ RUN POWERSHELL_RELEASE=$(curl -fSsL "https://aka.ms/powershell-release?tag=lts" && chmod +x /opt/microsoft/powershell/pwsh # Prepare Go distribution -ARG GO_VERSION="1.16.7" +ARG GO_VERSION="1.17.5" RUN curl -fSsL "https://golang.org/dl/go${GO_VERSION}.linux-${TARGETPLATFORM#linux/}.tar.gz" -o /tmp/go.tar.gz \ && mkdir -p /opt/golang/go \ && tar -xzf /tmp/go.tar.gz -C /opt/golang/go --strip-components=1 diff --git a/superchain/README.md b/superchain/README.md index 79e56fd767..e254208469 100644 --- a/superchain/README.md +++ b/superchain/README.md @@ -16,7 +16,7 @@ SDK | Version `Javascript` | `node >= 12.7.0` OR `node >= 14.16.0` OR `node >= 16.0.0` with `npm >= 6.14.11` (see [NodeJS and NPM](#nodejs-and-npm)) `PowerShell` | `pwsh >= 7.1.3` `Python 3` | `python3 >= 3.7.4` with `pip3 >= 20.0.2` -`Go` | `go >= 1.16` +`Go` | `go >= 1.17` ## Image tags diff --git a/tools/jsii-compliance/suite.ts b/tools/jsii-compliance/suite.ts index ba895b6706..d32d9b849a 100644 --- a/tools/jsii-compliance/suite.ts +++ b/tools/jsii-compliance/suite.ts @@ -510,6 +510,10 @@ export const suite: schema.Suite = { { name: 'classCanBeUsedWhenNotExpressedlyLoaded', description: 'Validates that types not explicitly loaded by the user can safely be returned by JS code', + }, + { + name: 'downcasting', + description: 'Ensures unsafe-cast features work as expected', } ], };