This is an automated email from the ASF dual-hosted git repository.

alexstocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git


The following commit(s) were added to refs/heads/develop by this push:
     new e12d8c5f9 fix(generic): support variadic method invocation via generic 
call (#3284)
e12d8c5f9 is described below

commit e12d8c5f97885075361b468356703b1d91058167
Author: 承潜 <[email protected]>
AuthorDate: Mon Apr 13 13:20:09 2026 +0800

    fix(generic): support variadic method invocation via generic call (#3284)
    
    * fix(generic): support variadic method invocation via generic call
    
    * fix(generic): support variadic invocation across reflection paths
---
 common/constant/key.go                |  69 +++----
 common/rpc_service.go                 |   5 +
 filter/generic/service_filter.go      | 331 ++++++++++++++++++++++++++++++++--
 filter/generic/service_filter_test.go | 265 +++++++++++++++++++++++++++
 protocol/triple/server.go             |  85 ++++++---
 protocol/triple/server_test.go        |  28 +++
 proxy/proxy_factory/default.go        |  39 +++-
 proxy/proxy_factory/default_test.go   |  32 ++++
 proxy/proxy_factory/invoker_test.go   |  41 +++++
 proxy/proxy_factory/pass_through.go   |   2 +-
 proxy/proxy_factory/utils.go          |  11 +-
 proxy/proxy_factory/utils_test.go     |  71 ++++++--
 server/server.go                      |  29 ++-
 server/server_test.go                 |  26 +++
 14 files changed, 933 insertions(+), 101 deletions(-)

diff --git a/common/constant/key.go b/common/constant/key.go
index 2be7ffc88..aba807ced 100644
--- a/common/constant/key.go
+++ b/common/constant/key.go
@@ -28,40 +28,41 @@ const (
 )
 
 const (
-       GroupKey               = "group"
-       VersionKey             = "version"
-       InterfaceKey           = "interface"
-       MessageSizeKey         = "message_size"
-       PathKey                = "path"
-       ServiceKey             = "service"
-       MethodsKey             = "methods"
-       TimeoutKey             = "timeout"
-       CategoryKey            = "category"
-       CheckKey               = "check"
-       EnabledKey             = "enabled"
-       SideKey                = "side"
-       OverrideProvidersKey   = "providerAddresses"
-       BeanNameKey            = "bean.name"
-       GenericKey             = "generic"
-       ClassifierKey          = "classifier"
-       TokenKey               = "token"
-       LocalAddr              = "local-addr"
-       RemoteAddr             = "remote-addr"
-       DefaultRemotingTimeout = 1000
-       ReleaseKey             = "release"
-       AnyhostKey             = "anyhost"
-       PortKey                = "port"
-       ProtocolKey            = "protocol"
-       PathSeparator          = "/"
-       DotSeparator           = "."
-       CommaSeparator         = ","
-       SslEnabledKey          = "ssl-enabled"
-       ParamsTypeKey          = "parameter-type-names" // key used in pass 
through invoker factory, to define param type
-       MetadataTypeKey        = "metadata-type"
-       MaxCallSendMsgSize     = "max-call-send-msg-size"
-       MaxServerSendMsgSize   = "max-server-send-msg-size"
-       MaxCallRecvMsgSize     = "max-call-recv-msg-size"
-       MaxServerRecvMsgSize   = "max-server-recv-msg-size"
+       GroupKey                    = "group"
+       VersionKey                  = "version"
+       InterfaceKey                = "interface"
+       MessageSizeKey              = "message_size"
+       PathKey                     = "path"
+       ServiceKey                  = "service"
+       MethodsKey                  = "methods"
+       TimeoutKey                  = "timeout"
+       CategoryKey                 = "category"
+       CheckKey                    = "check"
+       EnabledKey                  = "enabled"
+       SideKey                     = "side"
+       OverrideProvidersKey        = "providerAddresses"
+       BeanNameKey                 = "bean.name"
+       GenericKey                  = "generic"
+       GenericVariadicCallSliceKey = "generic-variadic-call-slice" // internal 
marker from generic filter to variadic reflection dispatchers
+       ClassifierKey               = "classifier"
+       TokenKey                    = "token"
+       LocalAddr                   = "local-addr"
+       RemoteAddr                  = "remote-addr"
+       DefaultRemotingTimeout      = 1000
+       ReleaseKey                  = "release"
+       AnyhostKey                  = "anyhost"
+       PortKey                     = "port"
+       ProtocolKey                 = "protocol"
+       PathSeparator               = "/"
+       DotSeparator                = "."
+       CommaSeparator              = ","
+       SslEnabledKey               = "ssl-enabled"
+       ParamsTypeKey               = "parameter-type-names" // key used in 
pass through invoker factory, to define param type
+       MetadataTypeKey             = "metadata-type"
+       MaxCallSendMsgSize          = "max-call-send-msg-size"
+       MaxServerSendMsgSize        = "max-server-send-msg-size"
+       MaxCallRecvMsgSize          = "max-call-recv-msg-size"
+       MaxServerRecvMsgSize        = "max-server-recv-msg-size"
 
        // TODO: remove KeepAliveInterval and KeepAliveInterval in version 4.0.0
        KeepAliveInterval = "keep-alive-interval"
diff --git a/common/rpc_service.go b/common/rpc_service.go
index 84da0cd66..cc936d3c1 100644
--- a/common/rpc_service.go
+++ b/common/rpc_service.go
@@ -132,6 +132,11 @@ func (m *MethodType) ReplyType() reflect.Type {
        return m.replyType
 }
 
+// IsVariadic returns true if the method has a variadic (...T) final parameter.
+func (m *MethodType) IsVariadic() bool {
+       return m.method.Type.IsVariadic()
+}
+
 // SuiteContext transfers @ctx to reflect.Value type or get it from @m.ctxType.
 func (m *MethodType) SuiteContext(ctx context.Context) reflect.Value {
        if ctxV := reflect.ValueOf(ctx); ctxV.IsValid() {
diff --git a/filter/generic/service_filter.go b/filter/generic/service_filter.go
index 16925f771..0a986a9c8 100644
--- a/filter/generic/service_filter.go
+++ b/filter/generic/service_filter.go
@@ -19,6 +19,8 @@ package generic
 
 import (
        "context"
+       "reflect"
+       "strings"
        "sync"
 )
 
@@ -35,7 +37,9 @@ import (
        "dubbo.apache.org/dubbo-go/v3/common/constant"
        "dubbo.apache.org/dubbo-go/v3/common/extension"
        "dubbo.apache.org/dubbo-go/v3/filter"
+       "dubbo.apache.org/dubbo-go/v3/filter/generic/generalizer"
        "dubbo.apache.org/dubbo-go/v3/protocol/base"
+       dubboHessian "dubbo.apache.org/dubbo-go/v3/protocol/dubbo/hessian2"
        "dubbo.apache.org/dubbo-go/v3/protocol/invocation"
        "dubbo.apache.org/dubbo-go/v3/protocol/result"
 )
@@ -78,44 +82,339 @@ func (f *genericServiceFilter) Invoke(ctx context.Context, 
invoker base.Invoker,
        `, mtdName, types, args)
 
        // get the type of the argument
-       ivkUrl := invoker.GetURL()
-       svc := common.ServiceMap.GetServiceByServiceKey(ivkUrl.Protocol, 
ivkUrl.ServiceKey())
+       ivkURL := invoker.GetURL()
+       svc := common.ServiceMap.GetServiceByServiceKey(ivkURL.Protocol, 
ivkURL.ServiceKey())
        method := svc.Method()[mtdName]
        if method == nil {
                return &result.RPCResult{
-                       Err: perrors.Errorf("\"%s\" method is not found, 
service key: %s", mtdName, ivkUrl.ServiceKey()),
+                       Err: perrors.Errorf("\"%s\" method is not found, 
service key: %s", mtdName, ivkURL.ServiceKey()),
                }
        }
+
        argsType := method.ArgsType()
+       if err := validateGenericArgs(method.IsVariadic(), len(argsType), 
len(args), mtdName); err != nil {
+               return &result.RPCResult{Err: err}
+       }
 
        // get generic info from attachments of invocation, the default value 
is "true"
        generic := inv.GetAttachmentWithDefaultValue(constant.GenericKey, 
constant.GenericSerializationDefault)
        // get generalizer according to value in the `generic`
-       g := getGeneralizer(generic)
+       // realize
+       newArgs, err := realizeInvocationArgs(getGeneralizer(generic), 
argsType, args, method.IsVariadic(), types)
+       if err != nil {
+               return &result.RPCResult{Err: err}
+       }
 
-       if len(args) != len(argsType) {
-               return &result.RPCResult{
-                       Err: perrors.Errorf("the number of args(=%d) is not 
matched with \"%s\" method", len(args), mtdName),
+       newIvc := invocation.NewRPCInvocation(mtdName, newArgs, 
inv.Attachments())
+       newIvc.SetReply(inv.Reply())
+       if method.IsVariadic() {
+               newIvc.SetAttribute(constant.GenericVariadicCallSliceKey, true)
+       }
+
+       return invoker.Invoke(ctx, newIvc)
+}
+
+// validateGenericArgs checks the generic arg count against the method 
signature.
+// Variadic methods accept any count >= the fixed parameter count.
+func validateGenericArgs(isVariadic bool, argsTypeCount, argCount int, 
methodName string) error {
+       if isVariadic {
+               if argCount >= argsTypeCount-1 {
+                       return nil
                }
+       } else if argCount == argsTypeCount {
+               return nil
        }
 
-       // realize
+       return perrors.Errorf("the number of args(=%d) is not matched with 
\"%s\" method", argCount, methodName)
+}
+
+// realizeInvocationArgs realizes generic args and packs a variadic tail into 
the declared slice type.
+func realizeInvocationArgs(g generalizer.Generalizer, argsType []reflect.Type, 
args []hessian.Object, isVariadic bool, types any) ([]any, error) {
+       if !isVariadic {
+               return realizeFixedArgs(g, args, argsType)
+       }
+
+       newArgs, err := realizeFixedArgs(g, args[:len(argsType)-1], 
argsType[:len(argsType)-1])
+       if err != nil {
+               return nil, err
+       }
+
+       variadicArg, err := realizeVariadicArg(g, args[len(argsType)-1:], 
argsType[len(argsType)-1], variadicTypeName(types))
+       if err != nil {
+               return nil, err
+       }
+
+       return append(newArgs, variadicArg), nil
+}
+
+// realizeFixedArgs realizes non-variadic parameters one by one.
+func realizeFixedArgs(g generalizer.Generalizer, args []hessian.Object, 
argsType []reflect.Type) ([]any, error) {
        newArgs := make([]any, len(argsType))
-       for i := 0; i < len(argsType); i++ {
+       for i := range argsType {
                newArg, err := g.Realize(args[i], argsType[i])
                if err != nil {
-                       return &result.RPCResult{
-                               Err: perrors.Errorf("realization failed, %v", 
err),
-                       }
+                       return nil, perrors.Errorf("realization of arg[%d] 
failed: %v", i, err)
                }
                newArgs[i] = newArg
        }
 
-       // build a normal invocation
-       newIvc := invocation.NewRPCInvocation(mtdName, newArgs, 
inv.Attachments())
-       newIvc.SetReply(inv.Reply())
+       return newArgs, nil
+}
 
-       return invoker.Invoke(ctx, newIvc)
+// realizeVariadicArg realizes the variadic tail into the declared slice type.
+// It unwraps a single arg only when its declared generic type matches the 
variadic slice.
+func realizeVariadicArg(g generalizer.Generalizer, args []hessian.Object, 
variadicSliceType reflect.Type, variadicType string) (any, error) {
+       variadicArgs := normalizeVariadicArgs(args, variadicSliceType, 
variadicType)
+       slice := reflect.MakeSlice(variadicSliceType, len(variadicArgs), 
len(variadicArgs))
+       elemType := variadicSliceType.Elem()
+
+       for i, arg := range variadicArgs {
+               realized, err := g.Realize(arg, elemType)
+               if err != nil {
+                       return nil, perrors.Errorf("realization of variadic 
arg[%d] failed: %v", i, err)
+               }
+               realizedValue, err := assignableValue(realized, elemType)
+               if err != nil {
+                       return nil, perrors.Errorf("realization of variadic 
arg[%d] failed: %v", i, err)
+               }
+               slice.Index(i).Set(realizedValue)
+       }
+
+       return slice.Interface(), nil
+}
+
+// assignableValue fits a realized value into the target type without 
panicking on Set.
+func assignableValue(value any, targetType reflect.Type) (reflect.Value, 
error) {
+       if value == nil {
+               if canBeNil(targetType) {
+                       return reflect.Zero(targetType), nil
+               }
+               return reflect.Value{}, perrors.Errorf("nil is not assignable 
to %s", targetType)
+       }
+
+       realizedValue := reflect.ValueOf(value)
+       if realizedValue.Type().AssignableTo(targetType) {
+               return realizedValue, nil
+       }
+       if realizedValue.Type().ConvertibleTo(targetType) {
+               return realizedValue.Convert(targetType), nil
+       }
+
+       return reflect.Value{}, perrors.Errorf("type %s is not assignable to 
%s", realizedValue.Type(), targetType)
+}
+
+func canBeNil(typ reflect.Type) bool {
+       switch typ.Kind() {
+       case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, 
reflect.Ptr, reflect.Slice:
+               return true
+       default:
+               return false
+       }
+}
+
+// variadicTypeName returns the last generic type name when the caller 
provides $invoke type metadata.
+func variadicTypeName(types any) string {
+       switch typeNames := types.(type) {
+       case []string:
+               if len(typeNames) == 0 {
+                       return ""
+               }
+               return typeNames[len(typeNames)-1]
+       case []any:
+               if len(typeNames) == 0 {
+                       return ""
+               }
+               if typeName, ok := typeNames[len(typeNames)-1].(string); ok {
+                       return typeName
+               }
+       }
+
+       return ""
+}
+
+// normalizeVariadicArgs unwraps one packed array only when the generic type 
says it
+// is the variadic slice itself; otherwise the single arg stays as one 
variadic value.
+func normalizeVariadicArgs(args []hessian.Object, variadicSliceType 
reflect.Type, variadicType string) []hessian.Object {
+       if len(args) != 1 {
+               return args
+       }
+       if !shouldUnwrapPackedVariadicArg(variadicType, variadicSliceType) {
+               if variadicType != "" {
+                       return args
+               }
+               return normalizeVariadicArgsWithoutType(args[0], 
variadicSliceType)
+       }
+       if args[0] == nil {
+               return nil
+       }
+
+       return unwrapToSlice(args[0])
+}
+
+// normalizeVariadicArgsWithoutType keeps dubbo-go compatibility when generic 
callers omit `types`.
+// A real single variadic value stays packed as one element, while a packed 
tail slice is unwrapped once.
+func normalizeVariadicArgsWithoutType(arg hessian.Object, variadicSliceType 
reflect.Type) []hessian.Object {
+       if arg == nil {
+               return nil
+       }
+
+       v := reflect.ValueOf(arg)
+       if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
+               return []hessian.Object{arg}
+       }
+
+       elemType := variadicSliceType.Elem()
+       argType := v.Type()
+       if elemType.Kind() != reflect.Interface && 
(argType.AssignableTo(elemType) || argType.ConvertibleTo(elemType)) {
+               return []hessian.Object{arg}
+       }
+
+       return unwrapToSlice(arg)
+}
+
+// shouldUnwrapPackedVariadicArg matches the declared variadic slice against 
the
+// generic tail type, including Java names and JVM array descriptors.
+func shouldUnwrapPackedVariadicArg(variadicType string, variadicSliceType 
reflect.Type) bool {
+       if variadicType == "" {
+               return false
+       }
+
+       for _, typeName := range javaTypeNamesForType(variadicSliceType) {
+               if variadicType == typeName {
+                       return true
+               }
+       }
+
+       elemType := variadicSliceType.Elem()
+       if elemType.Kind() == reflect.Interface && (variadicType == 
"[Ljava.lang.Object;" || variadicType == "java.lang.Object[]") {
+               return true
+       }
+
+       return false
+}
+
+// javaTypeNamesForType returns the generic type spellings we accept for the 
variadic slice.
+func javaTypeNamesForType(typ reflect.Type) []string {
+       zero := reflect.Zero(typ)
+       if !zero.IsValid() {
+               return nil
+       }
+
+       names := make([]string, 0, 2)
+       if name, err := dubboHessian.GetJavaName(zero.Interface()); err == nil 
&& name != "" {
+               names = append(names, name)
+       }
+       if desc := dubboHessian.GetClassDesc(zero.Interface()); desc != "" && 
desc != "V" {
+               names = appendUniqueString(names, desc)
+       }
+       if desc := jvmArrayDescriptorForType(typ); desc != "" {
+               names = appendUniqueString(names, desc)
+       }
+
+       return names
+}
+
+// jvmArrayDescriptorForType builds descriptors like [B, [[B or 
[[Ljava.lang.String;.
+func jvmArrayDescriptorForType(typ reflect.Type) string {
+       if typ.Kind() != reflect.Slice && typ.Kind() != reflect.Array {
+               return ""
+       }
+
+       depth := 0
+       for typ.Kind() == reflect.Slice || typ.Kind() == reflect.Array {
+               depth++
+               typ = typ.Elem()
+       }
+
+       leaf := jvmLeafDescriptorForType(typ)
+       if leaf == "" {
+               return ""
+       }
+
+       return strings.Repeat("[", depth) + leaf
+}
+
+func jvmLeafDescriptorForType(typ reflect.Type) string {
+       switch typ.Kind() {
+       case reflect.Bool:
+               return "Z"
+       case reflect.Int8, reflect.Uint8:
+               return "B"
+       case reflect.Int16:
+               return "S"
+       case reflect.Uint16:
+               return "C"
+       case reflect.Int, reflect.Int64:
+               return "J"
+       case reflect.Int32:
+               return "I"
+       case reflect.Float32:
+               return "F"
+       case reflect.Float64:
+               return "D"
+       case reflect.String:
+               return "Ljava.lang.String;"
+       case reflect.Interface:
+               return "Ljava.lang.Object;"
+       case reflect.Map:
+               return "Ljava.util.Map;"
+       case reflect.Struct:
+               if typ.PkgPath() == "time" && typ.Name() == "Time" {
+                       return "Ljava.util.Date;"
+               }
+               return "Ljava.lang.Object;"
+       default:
+               zero := reflect.New(typ).Elem().Interface()
+               desc := dubboHessian.GetClassDesc(zero)
+               switch desc {
+               case "", "V", "java.util.List":
+                       return ""
+               case "java.lang.String":
+                       return "Ljava.lang.String;"
+               case "java.lang.Object":
+                       return "Ljava.lang.Object;"
+               case "java.util.Date":
+                       return "Ljava.util.Date;"
+               case "java.util.Map":
+                       return "Ljava.util.Map;"
+               }
+               if len(desc) == 1 {
+                       return desc
+               }
+               if strings.HasPrefix(desc, "L") && strings.HasSuffix(desc, ";") 
{
+                       return desc
+               }
+               if strings.Contains(desc, ".") {
+                       return "L" + strings.ReplaceAll(desc, ".", "/") + ";"
+               }
+               return ""
+       }
+}
+
+func appendUniqueString(values []string, value string) []string {
+       for _, existing := range values {
+               if existing == value {
+                       return values
+               }
+       }
+       return append(values, value)
+}
+
+// unwrapToSlice returns slice/array elements, or keeps obj as one variadic 
element.
+func unwrapToSlice(obj hessian.Object) []hessian.Object {
+       if obj == nil {
+               return []hessian.Object{nil}
+       }
+       v := reflect.ValueOf(obj)
+       if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
+               out := make([]hessian.Object, v.Len())
+               for i := 0; i < v.Len(); i++ {
+                       out[i] = v.Index(i).Interface()
+               }
+               return out
+       }
+       // not a collection — treat as single variadic element
+       return []hessian.Object{obj}
 }
 
 func (f *genericServiceFilter) OnResponse(_ context.Context, result 
result.Result, _ base.Invoker, inv base.Invocation) result.Result {
diff --git a/filter/generic/service_filter_test.go 
b/filter/generic/service_filter_test.go
index b28d25e05..84756f42c 100644
--- a/filter/generic/service_filter_test.go
+++ b/filter/generic/service_filter_test.go
@@ -71,6 +71,22 @@ func (s *MockHelloService) HelloPB(req 
*generalizer.RequestType) (*generalizer.R
        return nil, perrors.Errorf("people not found")
 }
 
+func (s *MockHelloService) HelloVariadic(prefix string, names ...string) 
(string, error) {
+       return prefix, nil
+}
+
+func (s *MockHelloService) EchoVariadic(args ...any) ([]any, error) {
+       return args, nil
+}
+
+func (s *MockHelloService) BytesVariadic(args ...[]byte) ([][]byte, error) {
+       return args, nil
+}
+
+func (s *MockHelloService) NestedStringVariadic(args ...[]string) ([][]string, 
error) {
+       return args, nil
+}
+
 func TestServiceFilter_Invoke(t *testing.T) {
        filter := &genericServiceFilter{}
 
@@ -214,3 +230,252 @@ func TestServiceFilter_OnResponse(t *testing.T) {
        response := filter.OnResponse(context.Background(), rpcResult, nil, 
invocation1)
        assert.Equal(t, "result", response.Result())
 }
+
+func TestServiceFilter_InvokeVariadic(t *testing.T) {
+       filter := &genericServiceFilter{}
+
+       ctrl := gomock.NewController(t)
+       defer ctrl.Finish()
+
+       mockInvoker := mock.NewMockInvoker(ctrl)
+       service := &MockHelloService{}
+       ivkURL := common.NewURLWithOptions(
+               common.WithProtocol("test-variadic"),
+               common.WithParams(url.Values{}),
+               common.WithParamsValue(constant.InterfaceKey, 
service.Reference()),
+               common.WithParamsValue(constant.GenericKey, 
constant.GenericSerializationDefault),
+       )
+       _, err := 
common.ServiceMap.Register(ivkURL.GetParam(constant.InterfaceKey, ""),
+               ivkURL.Protocol,
+               "",
+               "",
+               service)
+       require.NoError(t, err)
+       t.Cleanup(func() {
+               _ = 
common.ServiceMap.UnRegister(ivkURL.GetParam(constant.InterfaceKey, ""), 
ivkURL.Protocol, ivkURL.ServiceKey())
+       })
+
+       mockInvoker.EXPECT().GetURL().Return(ivkURL).AnyTimes()
+
+       cases := []struct {
+               name             string
+               inv              *invocation.RPCInvocation
+               assertInvocation func(t *testing.T, inv base.Invocation)
+       }{
+               {
+                       name: "fixed plus discrete variadic args",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "HelloVariadic",
+                               []string{"java.lang.String", 
"java.lang.String", "java.lang.String"},
+                               []hessian.Object{"hello", "alice", "bob"},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "HelloVariadic", 
inv.MethodName())
+                               assert.Equal(t, []any{"hello", 
[]string{"alice", "bob"}}, inv.Arguments())
+                       },
+               },
+               {
+                       name: "fixed plus single scalar variadic arg",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "HelloVariadic",
+                               []string{"java.lang.String", 
"java.lang.String"},
+                               []hessian.Object{"hello", "alice"},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "HelloVariadic", 
inv.MethodName())
+                               assert.Equal(t, []any{"hello", 
[]string{"alice"}}, inv.Arguments())
+                       },
+               },
+               {
+                       name: "fixed plus packed variadic array",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "HelloVariadic",
+                               []string{"java.lang.String", 
"[Ljava.lang.String;"},
+                               []hessian.Object{"hello", []any{"alice", 
"bob"}},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "HelloVariadic", 
inv.MethodName())
+                               assert.Equal(t, []any{"hello", 
[]string{"alice", "bob"}}, inv.Arguments())
+                       },
+               },
+               {
+                       name: "fixed plus packed variadic array without types",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "HelloVariadic",
+                               nil,
+                               []hessian.Object{"hello", []string{"alice", 
"bob"}},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "HelloVariadic", 
inv.MethodName())
+                               assert.Equal(t, []any{"hello", 
[]string{"alice", "bob"}}, inv.Arguments())
+                       },
+               },
+               {
+                       name: "fixed plus packed nil variadic array",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "HelloVariadic",
+                               []string{"java.lang.String", 
"[Ljava.lang.String;"},
+                               []hessian.Object{"hello", nil},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "HelloVariadic", 
inv.MethodName())
+                               assert.Equal(t, []any{"hello", []string{}}, 
inv.Arguments())
+                       },
+               },
+               {
+                       name: "zero variadic args",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "HelloVariadic",
+                               []string{"java.lang.String"},
+                               []hessian.Object{"hello"},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "HelloVariadic", 
inv.MethodName())
+                               assert.Equal(t, []any{"hello", []string{}}, 
inv.Arguments())
+                       },
+               },
+               {
+                       name: "interface variadic keeps nil element",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "EchoVariadic",
+                               []string{"java.lang.Object", 
"java.lang.Object"},
+                               []hessian.Object{nil, "tail"},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "EchoVariadic", 
inv.MethodName())
+                               assert.Equal(t, []any{[]any{nil, "tail"}}, 
inv.Arguments())
+                       },
+               },
+               {
+                       name: "interface variadic keeps a single nil element",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "EchoVariadic",
+                               []string{"java.lang.Object"},
+                               []hessian.Object{nil},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "EchoVariadic", 
inv.MethodName())
+                               assert.Equal(t, []any{[]any{nil}}, 
inv.Arguments())
+                       },
+               },
+               {
+                       name: "slice variadic keeps a single slice element",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "BytesVariadic",
+                               []string{"[B"},
+                               []hessian.Object{[]byte("tail")},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "BytesVariadic", 
inv.MethodName())
+                               assert.Equal(t, 
[]any{[][]byte{[]byte("tail")}}, inv.Arguments())
+                       },
+               },
+               {
+                       name: "slice variadic unwraps packed values without 
types",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "BytesVariadic",
+                               nil,
+                               []hessian.Object{[][]byte{[]byte("a"), 
[]byte("b")}},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "BytesVariadic", 
inv.MethodName())
+                               assert.Equal(t, []any{[][]byte{[]byte("a"), 
[]byte("b")}}, inv.Arguments())
+                       },
+               },
+               {
+                       name: "slice variadic unwraps nested byte descriptor",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "BytesVariadic",
+                               []string{"[[B"},
+                               []hessian.Object{[][]byte{[]byte("a"), 
[]byte("b")}},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "BytesVariadic", 
inv.MethodName())
+                               assert.Equal(t, []any{[][]byte{[]byte("a"), 
[]byte("b")}}, inv.Arguments())
+                       },
+               },
+               {
+                       name: "nested string variadic unwraps nested string 
descriptor",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "NestedStringVariadic",
+                               []string{"[[Ljava.lang.String;"},
+                               []hessian.Object{[][]string{{"a", "b"}, {"c"}}},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "NestedStringVariadic", 
inv.MethodName())
+                               assert.Equal(t, []any{[][]string{{"a", "b"}, 
{"c"}}}, inv.Arguments())
+                       },
+               },
+               {
+                       name: "interface variadic unwraps a packed object 
array",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "EchoVariadic",
+                               []string{"[Ljava.lang.Object;"},
+                               []hessian.Object{[]any{"alice", "bob"}},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "EchoVariadic", 
inv.MethodName())
+                               assert.Equal(t, []any{[]any{"alice", "bob"}}, 
inv.Arguments())
+                       },
+               },
+               {
+                       name: "interface variadic keeps packed nil object array 
empty",
+                       inv: invocation.NewRPCInvocation(constant.Generic, 
[]any{
+                               "EchoVariadic",
+                               []string{"[Ljava.lang.Object;"},
+                               []hessian.Object{nil},
+                       }, map[string]any{
+                               constant.GenericKey: 
constant.GenericSerializationDefault,
+                       }),
+                       assertInvocation: func(t *testing.T, inv 
base.Invocation) {
+                               assert.Equal(t, "EchoVariadic", 
inv.MethodName())
+                               assert.Equal(t, []any{[]any{}}, inv.Arguments())
+                       },
+               },
+       }
+
+       for _, tt := range cases {
+               t.Run(tt.name, func(t *testing.T) {
+                       mockInvoker.EXPECT().Invoke(gomock.Any(), 
gomock.Any()).DoAndReturn(
+                               func(_ context.Context, inv base.Invocation) 
result.Result {
+                                       marked, ok := 
inv.GetAttribute(constant.GenericVariadicCallSliceKey)
+                                       require.True(t, ok)
+                                       useCallSlice, ok := marked.(bool)
+                                       require.True(t, ok)
+                                       assert.True(t, useCallSlice)
+                                       tt.assertInvocation(t, inv)
+                                       return &result.RPCResult{}
+                               },
+                       ).Times(1)
+
+                       invokeResult := filter.Invoke(context.Background(), 
mockInvoker, tt.inv)
+                       require.NoError(t, invokeResult.Error())
+               })
+       }
+}
diff --git a/protocol/triple/server.go b/protocol/triple/server.go
index 0b31f3281..90e72bd6d 100644
--- a/protocol/triple/server.go
+++ b/protocol/triple/server.go
@@ -644,32 +644,7 @@ func buildMethodInfoWithReflection(methodType 
reflect.Method) *common.MethodInfo
                        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
+                       return callMethodByReflection(ctx, method, handler, 
args)
                },
        }
 }
@@ -700,6 +675,64 @@ func isReflectValueNil(v reflect.Value) bool {
        }
 }
 
+func callMethodByReflection(ctx context.Context, method reflect.Method, 
handler any, args []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))
+       }
+
+       var returnValues []reflect.Value
+       if shouldUseGenericVariadicCallSlice(ctx, method, args) {
+               returnValues = method.Func.CallSlice(in)
+       } else {
+               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
+}
+
+// shouldUseGenericVariadicCallSlice mirrors the ServiceInfo reflection gate 
for
+// Triple's reflection-based method dispatch.
+func shouldUseGenericVariadicCallSlice(ctx context.Context, method 
reflect.Method, args []any) bool {
+       if !method.Type.IsVariadic() || len(args) == 0 || len(args) != 
method.Type.NumIn()-2 {
+               return false
+       }
+
+       value, ok := 
ctx.Value(constant.DubboCtxKey(constant.GenericVariadicCallSliceKey)).(bool)
+       if !ok || !value {
+               return false
+       }
+
+       lastArg := args[len(args)-1]
+       if lastArg == nil {
+               return false
+       }
+
+       lastArgType := reflect.TypeOf(lastArg)
+       variadicSliceType := method.Type.In(method.Type.NumIn() - 1)
+       return lastArgType.AssignableTo(variadicSliceType) || 
lastArgType.ConvertibleTo(variadicSliceType)
+}
+
 // generateAttachments transfer http.Header to map[string]any and make all 
keys lowercase
 func generateAttachments(header http.Header) map[string]any {
        attachments := make(map[string]any, len(header))
diff --git a/protocol/triple/server_test.go b/protocol/triple/server_test.go
index b42e19d00..508b21118 100644
--- a/protocol/triple/server_test.go
+++ b/protocol/triple/server_test.go
@@ -922,6 +922,12 @@ func newServerForMethodHandlerTest() *Server {
        return &Server{triServer: tri.NewServer("127.0.0.1:0", nil)}
 }
 
+type tripleVariadicReflectionServiceForTest struct{}
+
+func (s *tripleVariadicReflectionServiceForTest) HelloVariadic(ctx 
context.Context, prefix string, names ...string) (string, error) {
+       return prefix + ":" + fmt.Sprint(len(names)), nil
+}
+
 func TestExtractUnaryInvocationArgs(t *testing.T) {
        t.Run("from non-idl argument slice", func(t *testing.T) {
                name := "alice"
@@ -937,6 +943,28 @@ func TestExtractUnaryInvocationArgs(t *testing.T) {
        })
 }
 
+func TestBuildMethodInfoWithReflectionVariadic(t *testing.T) {
+       svc := &tripleVariadicReflectionServiceForTest{}
+       method, ok := reflect.TypeOf(svc).MethodByName("HelloVariadic")
+       require.True(t, ok)
+
+       methodInfo := buildMethodInfoWithReflection(method)
+       require.NotNil(t, methodInfo)
+
+       t.Run("generic packed variadic tail uses call slice", func(t 
*testing.T) {
+               ctx := context.WithValue(context.Background(), 
constant.DubboCtxKey(constant.GenericVariadicCallSliceKey), true)
+               res, err := methodInfo.MethodFunc(ctx, []any{"hello", 
[]string{"alice", "bob"}}, svc)
+               require.NoError(t, err)
+               assert.Equal(t, "hello:2", res)
+       })
+
+       t.Run("ordinary discrete variadic call remains unchanged", func(t 
*testing.T) {
+               res, err := methodInfo.MethodFunc(context.Background(), 
[]any{"hello", "alice", "bob"}, svc)
+               require.NoError(t, err)
+               assert.Equal(t, "hello:2", res)
+       })
+}
+
 func TestWrapTripleResponse(t *testing.T) {
        resp := tri.NewResponse("already-wrapped")
        assert.Same(t, resp, wrapTripleResponse(resp))
diff --git a/proxy/proxy_factory/default.go b/proxy/proxy_factory/default.go
index b74e5bef7..cc52569a4 100644
--- a/proxy/proxy_factory/default.go
+++ b/proxy/proxy_factory/default.go
@@ -130,7 +130,8 @@ func (pi *ProxyInvoker) Invoke(ctx context.Context, 
invocation base.Invocation)
        }
 
        // prepare argv
-       if (len(method.ArgsType()) == 1 || len(method.ArgsType()) == 2 && 
method.ReplyType() == nil) && method.ArgsType()[0].String() == "[]interface {}" 
{
+       useCallSlice := shouldUseGenericVariadicCallSlice(invocation, method, 
args)
+       if !useCallSlice && (len(method.ArgsType()) == 1 || 
len(method.ArgsType()) == 2 && method.ReplyType() == nil) && 
method.ArgsType()[0].String() == "[]interface {}" {
                in = append(in, reflect.ValueOf(args))
        } else {
                for i := 0; i < len(args); i++ {
@@ -150,7 +151,7 @@ func (pi *ProxyInvoker) Invoke(ctx context.Context, 
invocation base.Invocation)
        var replyv reflect.Value
        var retErr any
 
-       returnValues, callErr := callLocalMethod(method.Method(), in)
+       returnValues, callErr := callLocalMethod(method.Method(), in, 
useCallSlice)
 
        if callErr != nil {
                logger.Errorf("Invoke function error: %+v, service: %#v", 
callErr, url)
@@ -176,6 +177,33 @@ func (pi *ProxyInvoker) Invoke(ctx context.Context, 
invocation base.Invocation)
        return result
 }
 
+// shouldUseGenericVariadicCallSlice only enables CallSlice for the generic 
variadic path
+// after the filter has already packed the variadic tail into the declared 
slice type.
+func shouldUseGenericVariadicCallSlice(invocation base.Invocation, method 
*common.MethodType, args []any) bool {
+       if !method.IsVariadic() || len(args) != len(method.ArgsType()) || 
len(args) == 0 {
+               return false
+       }
+
+       value, ok := 
invocation.GetAttribute(constant.GenericVariadicCallSliceKey)
+       if !ok {
+               return false
+       }
+
+       useCallSlice, ok := value.(bool)
+       if !ok || !useCallSlice {
+               return false
+       }
+
+       lastArg := args[len(args)-1]
+       if lastArg == nil {
+               return true
+       }
+
+       lastArgType := reflect.TypeOf(lastArg)
+       variadicSliceType := method.ArgsType()[len(method.ArgsType())-1]
+       return lastArgType.AssignableTo(variadicSliceType) || 
lastArgType.ConvertibleTo(variadicSliceType)
+}
+
 func getProviderURL(url *common.URL) *common.URL {
        if url.SubURL == nil {
                return url
@@ -203,6 +231,13 @@ func (tpi *infoProxyInvoker) Invoke(ctx context.Context, 
invocation base.Invocat
        args := invocation.Arguments()
        result := new(result.RPCResult)
        if method, ok := tpi.methodMap[name]; ok {
+               // ServiceInfo reflection paths only receive ctx/args, so carry 
the generic
+               // variadic marker through ctx for the downstream CallSlice 
check.
+               if marked, ok := 
invocation.GetAttribute(constant.GenericVariadicCallSliceKey); ok {
+                       if useCallSlice, ok := marked.(bool); ok && 
useCallSlice {
+                               ctx = context.WithValue(ctx, 
constant.DubboCtxKey(constant.GenericVariadicCallSliceKey), true)
+                       }
+               }
                res, err := method.MethodFunc(ctx, args, tpi.svc)
                result.SetResult(res)
                if err != nil {
diff --git a/proxy/proxy_factory/default_test.go 
b/proxy/proxy_factory/default_test.go
index 28f48ef9e..6c7c300a7 100644
--- a/proxy/proxy_factory/default_test.go
+++ b/proxy/proxy_factory/default_test.go
@@ -18,17 +18,21 @@
 package proxy_factory
 
 import (
+       "context"
        "fmt"
        "testing"
 )
 
 import (
        "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
 )
 
 import (
        "dubbo.apache.org/dubbo-go/v3/common"
+       "dubbo.apache.org/dubbo-go/v3/common/constant"
        "dubbo.apache.org/dubbo-go/v3/protocol/base"
+       "dubbo.apache.org/dubbo-go/v3/protocol/invocation"
 )
 
 func TestGetProxy(t *testing.T) {
@@ -58,3 +62,31 @@ func TestGetInvoker(t *testing.T) {
        invoker := proxyFactory.GetInvoker(url)
        assert.True(t, invoker.IsAvailable())
 }
+
+func TestInfoProxyInvoker_InvokePropagatesGenericVariadicMarker(t *testing.T) {
+       info := &common.ServiceInfo{
+               Methods: []common.MethodInfo{
+                       {
+                               Name: "HelloVariadic",
+                               MethodFunc: func(ctx context.Context, args 
[]any, handler any) (any, error) {
+                                       marked, ok := 
ctx.Value(constant.DubboCtxKey(constant.GenericVariadicCallSliceKey)).(bool)
+                                       require.True(t, ok)
+                                       assert.True(t, marked)
+                                       assert.Equal(t, []any{"hello", 
[]string{"alice", "bob"}}, args)
+                                       return "ok", nil
+                               },
+                       },
+               },
+       }
+
+       invoker := newInfoInvoker(common.NewURLWithOptions(), info, struct{}{})
+       inv := invocation.NewRPCInvocationWithOptions(
+               invocation.WithMethodName("HelloVariadic"),
+               invocation.WithArguments([]any{"hello", []string{"alice", 
"bob"}}),
+       )
+       inv.SetAttribute(constant.GenericVariadicCallSliceKey, true)
+
+       res := invoker.Invoke(context.Background(), inv)
+       require.NoError(t, res.Error())
+       assert.Equal(t, "ok", res.Result())
+}
diff --git a/proxy/proxy_factory/invoker_test.go 
b/proxy/proxy_factory/invoker_test.go
index 5757f6e53..e71791c85 100644
--- a/proxy/proxy_factory/invoker_test.go
+++ b/proxy/proxy_factory/invoker_test.go
@@ -43,6 +43,14 @@ func (s *ProxyInvokerService) Hello(_ context.Context, name 
string) (string, err
        return "hello:" + name, nil
 }
 
+func (s *ProxyInvokerService) EchoVariadic(args ...any) ([]any, error) {
+       return args, nil
+}
+
+func (s *ProxyInvokerService) CountByteSlices(args ...[]byte) (int, error) {
+       return len(args), nil
+}
+
 type PassThroughService struct{}
 
 func (s *PassThroughService) Service(method string, argTypes []string, args 
[][]byte, attachments map[string]any) (any, error) {
@@ -148,3 +156,36 @@ func TestPassThroughProxyInvoker_Invoke(t *testing.T) {
                assert.EqualError(t, result.Error(), "the param type is not 
[]byte")
        })
 }
+
+func TestProxyInvoker_InvokeVariadicCallSliceGating(t *testing.T) {
+       const (
+               protocol      = "test-variadic-protocol"
+               interfaceName = "ProxyInvokerVariadicService"
+       )
+       registerService(t, protocol, interfaceName, &ProxyInvokerService{})
+       u := newURL(protocol, interfaceName)
+       invoker := &ProxyInvoker{BaseInvoker: *base.NewBaseInvoker(u)}
+
+       t.Run("generic variadic marker expands packed slice", func(t 
*testing.T) {
+               inv := invocation.NewRPCInvocationWithOptions(
+                       invocation.WithMethodName("EchoVariadic"),
+                       invocation.WithArguments([]any{[]any{"alice", "bob"}}),
+               )
+               inv.SetAttribute(constant.GenericVariadicCallSliceKey, true)
+
+               res := invoker.Invoke(context.Background(), inv)
+               require.NoError(t, res.Error())
+               assert.Equal(t, []any{"alice", "bob"}, res.Result())
+       })
+
+       t.Run("ordinary slice variadic element does not trigger call slice", 
func(t *testing.T) {
+               inv := invocation.NewRPCInvocationWithOptions(
+                       invocation.WithMethodName("CountByteSlices"),
+                       invocation.WithArguments([]any{[]byte("alice")}),
+               )
+
+               res := invoker.Invoke(context.Background(), inv)
+               require.NoError(t, res.Error())
+               assert.Equal(t, 1, res.Result())
+       })
+}
diff --git a/proxy/proxy_factory/pass_through.go 
b/proxy/proxy_factory/pass_through.go
index 21083ac6b..edc24d0fc 100644
--- a/proxy/proxy_factory/pass_through.go
+++ b/proxy/proxy_factory/pass_through.go
@@ -112,7 +112,7 @@ func (pi *PassThroughProxyInvoker) Invoke(ctx 
context.Context, invocation base.I
        var replyv reflect.Value
        var retErr any
 
-       returnValues, callErr := callLocalMethod(method.Method(), in)
+       returnValues, callErr := callLocalMethod(method.Method(), in, false)
 
        if callErr != nil {
                logger.Errorf("Invoke function error: %+v, service: %#v", 
callErr, url)
diff --git a/proxy/proxy_factory/utils.go b/proxy/proxy_factory/utils.go
index 44af7d28c..1d5a7dc13 100644
--- a/proxy/proxy_factory/utils.go
+++ b/proxy/proxy_factory/utils.go
@@ -26,8 +26,9 @@ import (
        perrors "github.com/pkg/errors"
 )
 
-// CallLocalMethod is used to handle invoke exception in user func.
-func callLocalMethod(method reflect.Method, in []reflect.Value) 
([]reflect.Value, error) {
+// callLocalMethod invokes a local method and recovers panics.
+// useCallSlice is reserved for generic calls that already carry a packed 
variadic tail.
+func callLocalMethod(method reflect.Method, in []reflect.Value, useCallSlice 
bool) ([]reflect.Value, error) {
        var (
                returnValues []reflect.Value
                retErr       error
@@ -46,7 +47,11 @@ func callLocalMethod(method reflect.Method, in 
[]reflect.Value) ([]reflect.Value
                        }
                }()
 
-               returnValues = method.Func.Call(in)
+               if useCallSlice {
+                       returnValues = method.Func.CallSlice(in)
+               } else {
+                       returnValues = method.Func.Call(in)
+               }
        }()
 
        if retErr != nil {
diff --git a/proxy/proxy_factory/utils_test.go 
b/proxy/proxy_factory/utils_test.go
index 4afd7fdc3..95d8bccba 100644
--- a/proxy/proxy_factory/utils_test.go
+++ b/proxy/proxy_factory/utils_test.go
@@ -45,19 +45,25 @@ func (s *callLocalMethodSample) PanicUnknown() {
        panic(123)
 }
 
+func (s *callLocalMethodSample) EchoVariadic(args ...any) []any {
+       return args
+}
+
 func TestCallLocalMethod(t *testing.T) {
        sample := &callLocalMethodSample{}
        cases := []struct {
-               name      string
-               method    string
-               in        []reflect.Value
-               assertErr func(t *testing.T, err error)
-               assertOut func(t *testing.T, out []reflect.Value)
+               name         string
+               method       string
+               in           []reflect.Value
+               useCallSlice bool
+               assertErr    func(t *testing.T, err error)
+               assertOut    func(t *testing.T, out []reflect.Value)
        }{
                {
-                       name:   "call success",
-                       method: "Sum",
-                       in:     []reflect.Value{reflect.ValueOf(sample), 
reflect.ValueOf(1), reflect.ValueOf(2)},
+                       name:         "call success",
+                       method:       "Sum",
+                       in:           []reflect.Value{reflect.ValueOf(sample), 
reflect.ValueOf(1), reflect.ValueOf(2)},
+                       useCallSlice: false,
                        assertErr: func(t *testing.T, err error) {
                                assert.NoError(t, err)
                        },
@@ -67,29 +73,58 @@ func TestCallLocalMethod(t *testing.T) {
                        },
                },
                {
-                       name:   "panic with error",
-                       method: "PanicError",
-                       in:     []reflect.Value{reflect.ValueOf(sample)},
+                       name:         "panic with error",
+                       method:       "PanicError",
+                       in:           []reflect.Value{reflect.ValueOf(sample)},
+                       useCallSlice: false,
                        assertErr: func(t *testing.T, err error) {
                                assert.EqualError(t, err, "boom")
                        },
                },
                {
-                       name:   "panic with string",
-                       method: "PanicString",
-                       in:     []reflect.Value{reflect.ValueOf(sample)},
+                       name:         "panic with string",
+                       method:       "PanicString",
+                       in:           []reflect.Value{reflect.ValueOf(sample)},
+                       useCallSlice: false,
                        assertErr: func(t *testing.T, err error) {
                                assert.EqualError(t, err, "boom str")
                        },
                },
                {
-                       name:   "panic with unknown type",
-                       method: "PanicUnknown",
-                       in:     []reflect.Value{reflect.ValueOf(sample)},
+                       name:         "panic with unknown type",
+                       method:       "PanicUnknown",
+                       in:           []reflect.Value{reflect.ValueOf(sample)},
+                       useCallSlice: false,
                        assertErr: func(t *testing.T, err error) {
                                assert.EqualError(t, err, "invoke function 
error, unknow exception: 123")
                        },
                },
+               {
+                       name:         "variadic slice stays packed without call 
slice",
+                       method:       "EchoVariadic",
+                       in:           []reflect.Value{reflect.ValueOf(sample), 
reflect.ValueOf([]any{"alice", "bob"})},
+                       useCallSlice: false,
+                       assertErr: func(t *testing.T, err error) {
+                               assert.NoError(t, err)
+                       },
+                       assertOut: func(t *testing.T, out []reflect.Value) {
+                               assert.Len(t, out, 1)
+                               assert.Equal(t, []any{[]any{"alice", "bob"}}, 
out[0].Interface())
+                       },
+               },
+               {
+                       name:         "variadic slice expands with call slice",
+                       method:       "EchoVariadic",
+                       in:           []reflect.Value{reflect.ValueOf(sample), 
reflect.ValueOf([]any{"alice", "bob"})},
+                       useCallSlice: true,
+                       assertErr: func(t *testing.T, err error) {
+                               assert.NoError(t, err)
+                       },
+                       assertOut: func(t *testing.T, out []reflect.Value) {
+                               assert.Len(t, out, 1)
+                               assert.Equal(t, []any{"alice", "bob"}, 
out[0].Interface())
+                       },
+               },
        }
 
        for _, tt := range cases {
@@ -98,7 +133,7 @@ func TestCallLocalMethod(t *testing.T) {
                        if !ok {
                                t.Fatalf("method %s not found", tt.method)
                        }
-                       out, err := callLocalMethod(m, tt.in)
+                       out, err := callLocalMethod(m, tt.in, tt.useCallSlice)
                        if tt.assertErr != nil {
                                tt.assertErr(t, err)
                        }
diff --git a/server/server.go b/server/server.go
index eacb0c391..cff53f39f 100644
--- a/server/server.go
+++ b/server/server.go
@@ -204,7 +204,12 @@ func CallMethodByReflection(ctx context.Context, method 
reflect.Method, handler
        for _, arg := range args {
                in = append(in, reflect.ValueOf(arg))
        }
-       returnValues := method.Func.Call(in)
+       var returnValues []reflect.Value
+       if shouldUseGenericVariadicCallSlice(ctx, method, args) {
+               returnValues = method.Func.CallSlice(in)
+       } else {
+               returnValues = method.Func.Call(in)
+       }
 
        // Process return values
        if len(returnValues) == 1 {
@@ -229,6 +234,28 @@ func CallMethodByReflection(ctx context.Context, method 
reflect.Method, handler
        return result, err
 }
 
+// shouldUseGenericVariadicCallSlice is the ServiceInfo reflection-side gate 
for
+// generic variadic calls whose tail has already been packed into the declared 
slice type.
+func shouldUseGenericVariadicCallSlice(ctx context.Context, method 
reflect.Method, args []any) bool {
+       if !method.Type.IsVariadic() || len(args) == 0 || len(args) != 
method.Type.NumIn()-2 {
+               return false
+       }
+
+       value, ok := 
ctx.Value(constant.DubboCtxKey(constant.GenericVariadicCallSliceKey)).(bool)
+       if !ok || !value {
+               return false
+       }
+
+       lastArg := args[len(args)-1]
+       if lastArg == nil {
+               return false
+       }
+
+       lastArgType := reflect.TypeOf(lastArg)
+       variadicSliceType := method.Type.In(method.Type.NumIn() - 1)
+       return lastArgType.AssignableTo(variadicSliceType) || 
lastArgType.ConvertibleTo(variadicSliceType)
+}
+
 // createReflectionMethodFunc creates a MethodFunc that calls the given method 
via reflection.
 func createReflectionMethodFunc(method reflect.Method) func(ctx 
context.Context, args []any, handler any) (any, error) {
        return func(ctx context.Context, args []any, handler any) (any, error) {
diff --git a/server/server_test.go b/server/server_test.go
index edced826a..9a4e309a7 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -33,6 +33,7 @@ import (
 
 import (
        "dubbo.apache.org/dubbo-go/v3/common"
+       "dubbo.apache.org/dubbo-go/v3/common/constant"
        "dubbo.apache.org/dubbo-go/v3/global"
 )
 
@@ -315,6 +316,12 @@ func (g *greetServiceForTest) Greet(ctx context.Context, 
req string) (string, er
 
 func (g *greetServiceForTest) Reference() string { return 
"greetServiceForTest" }
 
+type variadicReflectionServiceForTest struct{}
+
+func (s *variadicReflectionServiceForTest) HelloVariadic(ctx context.Context, 
prefix string, names ...string) (string, error) {
+       return prefix + ":" + strconv.Itoa(len(names)), nil
+}
+
 // TestEnhanceServiceInfoMethodFuncBackfillExactName verifies that
 // enhanceServiceInfo fills in MethodFunc when the ServiceInfo method name
 // matches the Go exported method name exactly (PascalCase).
@@ -353,6 +360,25 @@ func 
TestEnhanceServiceInfoMethodFuncBackfillJavaStyleName(t *testing.T) {
                "MethodFunc must be found via swapped-case lookup to avoid 
nil-func panic on lowercase-first method names")
 }
 
+func TestCallMethodByReflectionVariadic(t *testing.T) {
+       svc := &variadicReflectionServiceForTest{}
+       method, ok := reflect.TypeOf(svc).MethodByName("HelloVariadic")
+       require.True(t, ok)
+
+       t.Run("generic packed variadic tail uses call slice", func(t 
*testing.T) {
+               ctx := context.WithValue(context.Background(), 
constant.DubboCtxKey(constant.GenericVariadicCallSliceKey), true)
+               res, err := CallMethodByReflection(ctx, method, svc, 
[]any{"hello", []string{"alice", "bob"}})
+               require.NoError(t, err)
+               assert.Equal(t, "hello:2", res)
+       })
+
+       t.Run("ordinary discrete variadic call remains unchanged", func(t 
*testing.T) {
+               res, err := CallMethodByReflection(context.Background(), 
method, svc, []any{"hello", "alice", "bob"})
+               require.NoError(t, err)
+               assert.Equal(t, "hello:2", res)
+       })
+}
+
 // Test getMetadataPort with default protocol
 func TestGetMetadataPortWithDefaultProtocol(t *testing.T) {
        opts := defaultServerOptions()


Reply via email to