Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
import (
"dubbo.apache.org/dubbo-go/v3/common"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/config/generic"
"dubbo.apache.org/dubbo-go/v3/metadata"
"dubbo.apache.org/dubbo-go/v3/protocol/base"
"dubbo.apache.org/dubbo-go/v3/protocol/invocation"
Expand Down Expand Up @@ -130,6 +131,35 @@ func (cli *Client) NewService(service any, opts ...ReferenceOption) (*Connection
return cli.DialWithService(interfaceName, service, finalOpts...)
}

// NewGenericService creates a GenericService for making generic calls without pre-generated stubs.
// The referenceStr parameter specifies the service interface name (e.g., "org.apache.dubbo.samples.UserProvider").
//
// Example usage:
//
// genericService, err := cli.NewGenericService("org.apache.dubbo.samples.UserProvider",
// client.WithURL("tri://127.0.0.1:50052"),
// )
// if err != nil {
// panic(err)
// }
// result, err := genericService.Invoke(ctx, "QueryUser", []string{"org.apache.dubbo.samples.User"}, []hessian.Object{user})
func (cli *Client) NewGenericService(referenceStr string, opts ...ReferenceOption) (*generic.GenericService, error) {
finalOpts := []ReferenceOption{
WithIDL(constant.NONIDL),
WithGeneric(),
WithSerialization(constant.Hessian2Serialization),
}
finalOpts = append(finalOpts, opts...)

genericService := generic.NewGenericService(referenceStr)
_, err := cli.DialWithService(referenceStr, genericService, finalOpts...)
if err != nil {
return nil, err
}

return genericService, nil
}

func (cli *Client) Dial(interfaceName string, opts ...ReferenceOption) (*Connection, error) {
return cli.dial(interfaceName, nil, nil, opts...)
}
Expand Down
8 changes: 8 additions & 0 deletions client/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,14 @@ func WithGeneric() ReferenceOption {
}
}

// WithGenericType sets the generic serialization type for generic call
// Valid values: "true" (default), "gson", "protobuf", "protobuf-json"
func WithGenericType(genericType string) ReferenceOption {
return func(opts *ReferenceOptions) {
opts.Reference.Generic = genericType
}
}

func WithSticky() ReferenceOption {
return func(opts *ReferenceOptions) {
opts.Reference.Sticky = true
Expand Down
6 changes: 4 additions & 2 deletions common/constant/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,8 +413,10 @@ const (

// Generic Filter
const (
GenericSerializationDefault = "true"
GenericSerializationGson = "gson"
GenericSerializationDefault = "true"
GenericSerializationGson = "gson"
GenericSerializationProtobuf = "protobuf"
GenericSerializationProtobufJson = "protobuf-json"
)

// AdaptiveService Filter
Expand Down
52 changes: 49 additions & 3 deletions filter/generic/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,59 @@ func (f *genericFilter) Invoke(ctx context.Context, invoker base.Invoker, inv ba
types,
args,
}
newIvc := invocation.NewRPCInvocation(constant.Generic, newArgs, inv.Attachments())
newIvc.SetReply(inv.Reply())

// For Triple protocol non-IDL mode, we need to set parameterRawValues
// The format is [param1, param2, ..., paramN, reply] where the last element is the reply placeholder
// Triple invoker slices as: request = inRaw[0:len-1], reply = inRaw[len-1]
// So for generic call, we need [methodName, types, args, reply] to get request = [methodName, types, args]
reply := inv.Reply()
parameterRawValues := []any{mtdName, types, args, reply}

newIvc := invocation.NewRPCInvocationWithOptions(
invocation.WithMethodName(constant.Generic),
invocation.WithArguments(newArgs),
invocation.WithParameterRawValues(parameterRawValues),
invocation.WithAttachments(inv.Attachments()),
invocation.WithReply(reply),
)
newIvc.Attachments()[constant.GenericKey] = invoker.GetURL().GetParam(constant.GenericKey, "")

// Copy CallType attribute from original invocation for Triple protocol support
// If not present, set default to CallUnary for generic calls
if callType, ok := inv.GetAttribute(constant.CallTypeKey); ok {
newIvc.SetAttribute(constant.CallTypeKey, callType)
} else {
newIvc.SetAttribute(constant.CallTypeKey, constant.CallUnary)
}

return invoker.Invoke(ctx, newIvc)
} else if isMakingAGenericCall(invoker, inv) {
inv.Attachments()[constant.GenericKey] = invoker.GetURL().GetParam(constant.GenericKey, "")
// Arguments format: [methodName string, types []string, args []hessian.Object]
oldArgs := inv.Arguments()
reply := inv.Reply()

// For Triple protocol non-IDL mode, we need to set parameterRawValues
// parameterRawValues format: [methodName, types, args, reply]
// Triple invoker slices as: request = inRaw[0:len-1], reply = inRaw[len-1]
parameterRawValues := []any{oldArgs[0], oldArgs[1], oldArgs[2], reply}

newIvc := invocation.NewRPCInvocationWithOptions(
invocation.WithMethodName(inv.MethodName()),
invocation.WithArguments(oldArgs),
invocation.WithParameterRawValues(parameterRawValues),
invocation.WithAttachments(inv.Attachments()),
invocation.WithReply(reply),
)
newIvc.Attachments()[constant.GenericKey] = invoker.GetURL().GetParam(constant.GenericKey, "")

// Set CallType for Triple protocol support
if callType, ok := inv.GetAttribute(constant.CallTypeKey); ok {
newIvc.SetAttribute(constant.CallTypeKey, callType)
} else {
newIvc.SetAttribute(constant.CallTypeKey, constant.CallUnary)
}

return invoker.Invoke(ctx, newIvc)
}
return invoker.Invoke(ctx, inv)
}
Expand Down
20 changes: 19 additions & 1 deletion protocol/triple/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,18 @@ func newClientManager(url *common.URL) (*clientManager, error) {

triClients := make(map[string]*tri.Client)

if len(url.Methods) != 0 {
// Check if this is a generic call - for generic call, we only need $invoke method
generic := url.GetParam(constant.GenericKey, "")
isGeneric := isGenericCall(generic)

if isGeneric {
// For generic call, only register $invoke method
invokeURL, err := joinPath(baseTriURL, url.Interface(), constant.Generic)
if err != nil {
return nil, fmt.Errorf("JoinPath failed for base %s, interface %s, method %s", baseTriURL, url.Interface(), constant.Generic)
}
triClients[constant.Generic] = tri.NewClient(httpClient, invokeURL, cliOpts...)
} else if len(url.Methods) != 0 {
for _, method := range url.Methods {
triURL, err := joinPath(baseTriURL, url.Interface(), method)
if err != nil {
Expand Down Expand Up @@ -312,6 +323,13 @@ func newClientManager(url *common.URL) (*clientManager, error) {
triClient := tri.NewClient(httpClient, triURL, cliOpts...)
triClients[methodName] = triClient
}

// Register $invoke method for generic call support in non-IDL mode
invokeURL, err := joinPath(baseTriURL, url.Interface(), constant.Generic)
if err != nil {
return nil, fmt.Errorf("JoinPath failed for base %s, interface %s, method %s", baseTriURL, url.Interface(), constant.Generic)
}
triClients[constant.Generic] = tri.NewClient(httpClient, invokeURL, cliOpts...)
}

return &clientManager{
Expand Down
122 changes: 86 additions & 36 deletions protocol/triple/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,51 +503,101 @@ func createServiceInfoWithReflection(svc common.RPCService) *common.ServiceInfo
if methodType.Name == "Reference" {
continue
}
paramsNum := methodType.Type.NumIn()
// the first param is receiver itself, the second param is ctx
// just ignore them
if paramsNum < 2 {
logger.Fatalf("TRIPLE does not support %s method that does not have any parameter", methodType.Name)
continue
}
paramsTypes := make([]reflect.Type, paramsNum-2)
for j := 2; j < paramsNum; j++ {
paramsTypes[j-2] = methodType.Type.In(j)
}
methodInfo := common.MethodInfo{
Name: methodType.Name,
// only support Unary invocation now
Type: constant.CallUnary,
ReqInitFunc: func() any {
params := make([]any, len(paramsTypes))
for k, paramType := range paramsTypes {
params[k] = reflect.New(paramType).Interface()
}
return params
},
methodInfo := buildMethodInfoWithReflection(methodType)
if methodInfo != nil {
methodInfos = append(methodInfos, *methodInfo)
}
methodInfos = append(methodInfos, methodInfo)
}

// only support no-idl mod call unary
genericMethodInfo := common.MethodInfo{
Name: "$invoke",
Type: constant.CallUnary,
// Add $invoke method for generic call support
methodInfos = append(methodInfos, buildGenericMethodInfo())

info.Methods = methodInfos
return &info
}

// buildMethodInfoWithReflection creates MethodInfo for a single method using reflection.
func buildMethodInfoWithReflection(methodType reflect.Method) *common.MethodInfo {
paramsNum := methodType.Type.NumIn()
// the first param is receiver itself, the second param is ctx
if paramsNum < 2 {
logger.Fatalf("TRIPLE does not support %s method that does not have any parameter", methodType.Name)
return nil
}

// Extract parameter types (skip receiver and context)
paramsTypes := make([]reflect.Type, paramsNum-2)
for j := 2; j < paramsNum; j++ {
paramsTypes[j-2] = methodType.Type.In(j)
}

// Capture method for closure
method := methodType
return &common.MethodInfo{
Name: methodType.Name,
Type: constant.CallUnary, // only support Unary invocation now
ReqInitFunc: func() any {
params := make([]any, 3)
// params must be pointer
params[0] = func(s string) *string { return &s }("methodName") // methodName *string
params[1] = &[]string{} // argv type *[]string
params[2] = &[]hessian.Object{} // argv *[]hessian.Object
params := make([]any, len(paramsTypes))
for k, paramType := range paramsTypes {
params[k] = reflect.New(paramType).Interface()
}
return params
},
MethodFunc: func(ctx context.Context, args []any, handler any) (any, error) {
in := []reflect.Value{reflect.ValueOf(handler)}
in = append(in, reflect.ValueOf(ctx))
for _, arg := range args {
in = append(in, reflect.ValueOf(arg))
}
returnValues := method.Func.Call(in)
if len(returnValues) == 1 {
if isReflectValueNil(returnValues[0]) {
return nil, nil
}
if err, ok := returnValues[0].Interface().(error); ok {
return nil, err
}
return nil, nil
}
var result any
var err error
if !isReflectValueNil(returnValues[0]) {
result = returnValues[0].Interface()
}
if len(returnValues) > 1 && !isReflectValueNil(returnValues[1]) {
if e, ok := returnValues[1].Interface().(error); ok {
err = e
}
}
return result, err
},
}
}

methodInfos = append(methodInfos, genericMethodInfo)

info.Methods = methodInfos
// buildGenericMethodInfo creates MethodInfo for $invoke generic call method.
func buildGenericMethodInfo() common.MethodInfo {
return common.MethodInfo{
Name: constant.Generic,
Type: constant.CallUnary,
ReqInitFunc: func() any {
return []any{
func(s string) *string { return &s }(""), // methodName *string
&[]string{}, // types *[]string
&[]hessian.Object{}, // args *[]hessian.Object
}
},
}
}

return &info
// isReflectValueNil safely checks if a reflect.Value is nil.
// It first checks if the value's kind supports nil checking to avoid panic.
func isReflectValueNil(v reflect.Value) bool {
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice, reflect.UnsafePointer:
return v.IsNil()
default:
return false
}
}

// generateAttachments transfer http.Header to map[string]any and make all keys lowercase
Expand Down
57 changes: 57 additions & 0 deletions protocol/triple/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import (
"context"
"fmt"
"net/http"
"reflect"
"sync"
"testing"
"unsafe"
)

import (
Expand Down Expand Up @@ -485,3 +487,58 @@ func Test_createServiceInfoWithReflection(t *testing.T) {
assert.Len(t, paramsSlice, 3) // methodName, argv types, argv
})
}

// Test isReflectValueNil safely checks if a reflect.Value is nil
func Test_isReflectValueNil(t *testing.T) {
tests := []struct {
name string
value any
expected bool
}{
// nil nillable types
{"nil chan", (chan int)(nil), true},
{"nil map", (map[string]int)(nil), true},
{"nil slice", ([]int)(nil), true},
{"nil func", (func())(nil), true},
{"nil pointer", (*int)(nil), true},
{"nil unsafe.Pointer", unsafe.Pointer(nil), true},

// non-nil nillable types
{"non-nil chan", make(chan int), false},
{"non-nil map", map[string]int{"a": 1}, false},
{"non-nil slice", []int{1, 2, 3}, false},
{"non-nil func", func() {}, false},
{"non-nil pointer", new(int), false},

// non-nillable types (should return false, not panic)
{"int", 42, false},
{"string", "hello", false},
{"bool", true, false},
{"float64", 3.14, false},
{"struct", struct{ Name string }{"test"}, false},
{"array", [3]int{1, 2, 3}, false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := reflect.ValueOf(tt.value)
// should not panic
result := isReflectValueNil(v)
assert.Equal(t, tt.expected, result)
})
}
}

// Test isReflectValueNil with UnsafePointer specifically
func Test_isReflectValueNil_UnsafePointer(t *testing.T) {
t.Run("nil unsafe.Pointer", func(t *testing.T) {
v := reflect.ValueOf(unsafe.Pointer(nil))
assert.True(t, isReflectValueNil(v))
})

t.Run("non-nil unsafe.Pointer", func(t *testing.T) {
x := 42
v := reflect.ValueOf(unsafe.Pointer(&x))
assert.False(t, isReflectValueNil(v))
})
}
Loading
Loading