Skip to content

Commit

Permalink
feat(go): add UnsafeCast function (#3316)
Browse files Browse the repository at this point in the history
The `UnsafeCast` function can be used to forcefully convert from one
type of jsii proxy value (including `interface{}`) to another jsii
interface (i.e: a class or interface instance).

If performs a "clean" cast when possible, and creates a new, aliased
proxy otherwise. It is the user's responsibility to ensure they are
passing the correct arguments into the function.

Fixes #2819

---

By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license].

[Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
  • Loading branch information
Romain Marcadier committed Jan 12, 2022
1 parent ad6ce99 commit 19da85e
Show file tree
Hide file tree
Showing 25 changed files with 373 additions and 78 deletions.
26 changes: 13 additions & 13 deletions .github/workflows/main.yml
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion gh-pages/content/specification/6-compliance-report.md
Expand Up @@ -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") | 🟢 | 🟢 |||
Expand Down Expand Up @@ -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") || 🟢 |||
10 changes: 10 additions & 0 deletions packages/@jsii/go-runtime-test/project/compliance_test.go
Expand Up @@ -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) {
Expand Down
13 changes: 12 additions & 1 deletion 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
Expand All @@ -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
Expand Down
6 changes: 4 additions & 2 deletions 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=
Expand Down Expand Up @@ -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=
Expand Up @@ -11,7 +11,7 @@ func New() abc.Cdk16625 {
return c
}

type cdk16625 struct{
type cdk16625 struct {
abc.Cdk16625
}

Expand Down
1 change: 1 addition & 0 deletions 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
Expand Down
59 changes: 59 additions & 0 deletions 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()))
}
101 changes: 101 additions & 0 deletions 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()
}
}
8 changes: 7 additions & 1 deletion 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
7 changes: 4 additions & 3 deletions 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=

0 comments on commit 19da85e

Please sign in to comment.