Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing RPC Function Calls with Reflection in Go

Tech 2

Reflection in Go enables dynamic function invocation, which can be leveraged to build a simple RPC system without modifying protocol definitions for each new service. This approach supports a range of parameter types including boolean, integer, floating-point, string, and byte slice.

Supported Paramter Types bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, string, byte, []byte

Target Function for Invocation

func DemoReflectFunc(paramBool bool, paramInt int, paramInt8 int8, paramInt16 int16, paramInt32 int32, paramInt64 int64, paramUint uint, paramUint8 uint8, paramUint16 uint16, paramUint32 uint32, paramUint64 uint64, paramFloat32 float32, paramFloat64 float64, paramString string, paramByte byte, paramBytes []byte) {
    fmt.Println("paramBool:", paramBool)
    fmt.Println("paramInt:", paramInt)
    fmt.Println("paramInt8:", paramInt8)
    fmt.Println("paramInt16:", paramInt16)
    fmt.Println("paramInt32:", paramInt32)
    fmt.Println("paramInt64:", paramInt64)
    fmt.Println("paramUint:", paramUint)
    fmt.Println("paramUint8:", paramUint8)
    fmt.Println("paramUint16:", paramUint16)
    fmt.Println("paramUint32:", paramUint32)
    fmt.Println("paramUint64:", paramUint64)
    fmt.Println("paramFloat32:", paramFloat32)
    fmt.Println("paramFloat64:", paramFloat64)
    fmt.Println("paramString:", paramString)
    fmt.Println("paramByte:", paramByte)
    var dataStruct *CustomData
    err := json.Unmarshal(paramBytes, &dataStruct)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("dataStruct:", dataStruct)
}

Registry for Reflection Functions

var (
    functionRegistry = map[string]interface{}{}
)

func initializeRegistry() {
    registerFunction(DemoReflectFunc)
}

func registerFunction(funcInterface interface{}) {
    funcName := runtime.FuncForPC(reflect.ValueOf(funcInterface).Pointer()).Name()
    funcType := reflect.TypeOf(funcInterface)
    if funcType.Kind() != reflect.Func {
        return
    }
    functionRegistry[funcName] = funcInterface
}

Function Call Mechanism This section ancodes data in to bytes; network transmission must be implemented separately.

func InvokeFunction[T any](targetFunc T, arguments ...interface{}) {
    funcName := runtime.FuncForPC(reflect.ValueOf(targetFunc).Pointer()).Name()
    funcType := reflect.TypeOf(targetFunc)
    if funcType.Kind() != reflect.Func {
        return
    }
    paramCount := funcType.NumIn()
    if paramCount != len(arguments) {
        return
    }
    rpcPacket := RPCPacket{
        FunctionIdentifier: funcName,
        Parameters:         arguments,
    }
    encodedData, err := serialize(rpcPacket)
    if err != nil {
        fmt.Println(err)
        return
    }
    // TODO: Send encodedData to server
    // TODO: Server calls ProcessRPCRequest
    err = ProcessRPCRequest(encodedData)
    if err != nil {
        fmt.Println(err)
    }
}

Function Reception and Type Conversion After decoding, types may be converted to int64 or float64, requiring manual conversion back to original types.

func ProcessRPCRequest(encodedBytes []byte) (err error) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Error in ProcessRPCRequest:", r)
        }
    }()
    packet, err := deserialize(encodedBytes)
    if err != nil {
        return
    }
    funcValue := functionRegistry[packet.FunctionIdentifier]
    val := reflect.ValueOf(funcValue)
    if val.Kind() != reflect.Func {
        return
    }
    funcType := reflect.TypeOf(funcValue)
    if funcType.Kind() != reflect.Func {
        return
    }
    paramCount := funcType.NumIn()
    callParams := make([]reflect.Value, 0)
    for i := 0; i < paramCount; i++ {
        paramType := funcType.In(i)
        switch paramType.Kind() {
        case reflect.Int:
            callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(int)))
        case reflect.Int8:
            if v, ok := packet.Parameters[i].(int); ok {
                callParams = append(callParams, reflect.ValueOf(int8(v)))
            } else {
                callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(int8)))
            }
        case reflect.Int16:
            if v, ok := packet.Parameters[i].(int); ok {
                callParams = append(callParams, reflect.ValueOf(int16(v)))
            } else {
                callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(int16)))
            }
        case reflect.Int32:
            if v, ok := packet.Parameters[i].(int); ok {
                callParams = append(callParams, reflect.ValueOf(int32(v)))
            } else {
                callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(int32)))
            }
        case reflect.Int64:
            if v, ok := packet.Parameters[i].(int); ok {
                callParams = append(callParams, reflect.ValueOf(int64(v)))
            } else {
                callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(int64)))
            }
        case reflect.Uint:
            callParams = append(callParams, reflect.ValueOf(uint(packet.Parameters[i].(int))))
        case reflect.Uint8:
            if v, ok := packet.Parameters[i].(int); ok {
                callParams = append(callParams, reflect.ValueOf(uint8(v)))
            } else {
                callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(uint8)))
            }
        case reflect.Uint16:
            if v, ok := packet.Parameters[i].(int); ok {
                callParams = append(callParams, reflect.ValueOf(uint16(v)))
            } else {
                callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(uint16)))
            }
        case reflect.Uint32:
            if v, ok := packet.Parameters[i].(int); ok {
                callParams = append(callParams, reflect.ValueOf(uint32(v)))
            } else {
                callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(uint32)))
            }
        case reflect.Uint64:
            if v, ok := packet.Parameters[i].(int); ok {
                callParams = append(callParams, reflect.ValueOf(uint64(v)))
            } else {
                callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(uint64)))
            }
        case reflect.Float32:
            if v, ok := packet.Parameters[i].(float64); ok {
                callParams = append(callParams, reflect.ValueOf(float32(v)))
            } else {
                callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(float32)))
            }
        case reflect.Float64:
            callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(float64)))
        default:
            callParams = append(callParams, reflect.ValueOf(packet.Parameters[i]))
        }
    }
    val.Call(callParams)
    return
}

Serialization and Deserialization

type RPCPacket struct {
    FunctionIdentifier string
    Parameters         []interface{}
}

func serialize(packet RPCPacket) ([]byte, error) {
    var buffer bytes.Buffer
    encoder := gob.NewEncoder(&buffer)
    if err := encoder.Encode(packet); err != nil {
        return nil, err
    }
    return buffer.Bytes(), nil
}

func deserialize(data []byte) (RPCPacket, error) {
    buffer := bytes.NewBuffer(data)
    decoder := gob.NewDecoder(buffer)
    var packet RPCPacket
    if err := decoder.Decode(&packet); err != nil {
        return packet, err
    }
    return packet, nil
}

Testing the Implementation

func main() {
    initializeRegistry()
    testData := &CustomData{
        Field1:  []bool{true},
        Field2:  []int{2},
        Field3:  []int8{3},
        Field4:  []int16{4},
        Field5:  []int32{5},
        Field6:  []int64{6},
        Field7:  []uint{7},
        Field8:  []uint8{8},
        Field9:  []uint16{9},
        Field10: []uint32{10},
        Field11: []uint64{11},
        Field12: []float32{12},
        Field13: []float64{13},
        Field14: []string{"14"},
        Field15: []byte{15},
        Field17: map[string]int{"a1": 1, "a2": 2, "a3": 3},
        Field18: map[int]string{1: "a1", 2: "a2", 3: "a3"},
        Field19: map[string]float64{"a1": 1.1, "a2": 2.2, "a3": 3.3},
        Nested: &CustomData{
            Field17: map[string]int{"a4": 4, "a5": 5, "a6": 6},
            Field18: map[int]string{4: "a4", 5: "a5", 6: "a6"},
            Field19: map[string]float64{"a4": 4.4, "a5": 5.5, "a6": 6.6},
        },
    }
    jsonBytes, _ := json.Marshal(testData)
    InvokeFunction(DemoReflectFunc,
        true, math.MaxInt, math.MaxInt8, math.MaxInt16, math.MaxInt32,
        math.MaxInt64, math.MaxInt, math.MaxUint8, math.MaxUint16, math.MaxUint32,
        math.MaxInt64, math.MaxFloat32, math.MaxFloat64, "1234567890", byte(255), jsonBytes)
}

Output: paramBool: true paramInt: 9223372036854775807 paramInt8: 127 paramInt16: 32767 paramInt32: 2147483647 paramInt64: 9223372036854775807 paramUint: 9223372036854775807 paramUint8: 255 paramUint16: 65535 paramUint32: 4294967295 paramUint64: 9223372036854775807 paramFloat32: 3.4028235e+38 paramFloat64: 1.7976931348623157e+308 paramString: 1234567890 paramByte: 255 dataStruct: &{[true] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] map[a1:1 a2:2 a3:3] map[1:a1 2:a2 3:a3] map[a1:1.1 a2:2.2 a3:3.3] 0xc00013c9c0}

Tags: goReflection

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.