diff --git a/go/Makefile b/go/Makefile index 1ec0145dfd..1265efadad 100644 --- a/go/Makefile +++ b/go/Makefile @@ -21,7 +21,7 @@ install-tools-go1.22.0: install-build-tools install-dev-tools-go1.22.0 install-tools: install-tools-go1.22.0 -build: build-glide-core build-glide-client generate-protobuf +build: build-glide-client generate-protobuf go build ./... build-glide-core: diff --git a/go/api/base_client.go b/go/api/base_client.go new file mode 100644 index 0000000000..7d2271e214 --- /dev/null +++ b/go/api/base_client.go @@ -0,0 +1,107 @@ +// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// #cgo LDFLAGS: -L../target/release -lglide_rs +// #include "../lib.h" +// +// void successCallback(uintptr_t channelPtr, char *message); +// void failureCallback(uintptr_t channelPtr, char *errMessage, RequestErrorType errType); +import "C" + +import ( + "github.com/aws/glide-for-redis/go/glide/protobuf" + "google.golang.org/protobuf/proto" + "unsafe" +) + +//export successCallback +func successCallback(channelPtr C.uintptr_t, cResponse *C.char) { + response := C.GoString(cResponse) + goChannelPointer := uintptr(channelPtr) + resultChannel := *(*chan payload)(unsafe.Pointer(goChannelPointer)) + resultChannel <- payload{value: &response, error: nil} +} + +//export failureCallback +func failureCallback(channelPtr C.uintptr_t, cErrorMessage *C.char, cErrorType C.RequestErrorType) { + goChannelPointer := uintptr(channelPtr) + resultChannel := *(*chan payload)(unsafe.Pointer(goChannelPointer)) + resultChannel <- payload{value: nil, error: goError(cErrorType, cErrorMessage)} +} + +type connectionRequestConverter interface { + toProtobuf() *protobuf.ConnectionRequest +} + +type baseClient struct { + coreClient unsafe.Pointer +} + +func createClient(converter connectionRequestConverter) (unsafe.Pointer, error) { + request := converter.toProtobuf() + msg, err := proto.Marshal(request) + if err != nil { + return nil, err + } + + byteCount := len(msg) + requestBytes := C.CBytes(msg) + cResponse := (*C.struct_ConnectionResponse)(C.create_client_using_protobuf((*C.uchar)(requestBytes), C.uintptr_t(byteCount), (C.SuccessCallback)(unsafe.Pointer(C.successCallback)), (C.FailureCallback)(unsafe.Pointer(C.failureCallback)))) + defer C.free_connection_response(cResponse) + + cErr := cResponse.error_message + if cErr != nil { + return nil, goError(cResponse.error_type, cResponse.error_message) + } + + return cResponse.conn_ptr, nil +} + +// Close terminates the client by closing all associated resources. +func (client *baseClient) Close() error { + if client.coreClient == nil { + return &GlideError{"The glide client was not open. Either it was not initialized, or it was already closed."} + } + + C.close_client(client.coreClient) + client.coreClient = nil + return nil +} + +func (client *baseClient) CustomCommand(args []string) (interface{}, error) { + cArgs := toCStrings(args) + defer freeCStrings(cArgs) + + resultChannel := make(chan payload) + resultChannelPtr := uintptr(unsafe.Pointer(&resultChannel)) + + requestType := C.uint32_t(customCommand) + C.command(client.coreClient, C.uintptr_t(resultChannelPtr), requestType, C.uintptr_t(len(args)), &cArgs[0]) + + payload := <-resultChannel + if payload.error != nil { + return nil, payload.error + } + + if payload.value != nil { + return *payload.value, nil + } + + return nil, nil +} + +func toCStrings(args []string) []*C.char { + cArgs := make([]*C.char, len(args)) + for i, arg := range args { + cString := C.CString(arg) + cArgs[i] = cString + } + return cArgs +} + +func freeCStrings(cArgs []*C.char) { + for _, arg := range cArgs { + C.free(unsafe.Pointer(arg)) + } +} diff --git a/go/api/cluster_client.go b/go/api/cluster_client.go new file mode 100644 index 0000000000..b74c299ea4 --- /dev/null +++ b/go/api/cluster_client.go @@ -0,0 +1,18 @@ +// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// RedisClusterClient is a client used for connection to cluster Redis servers. +type RedisClusterClient struct { + baseClient +} + +// CreateClusterClient creates a Redis client in cluster mode using the given [RedisClusterClientConfiguration]. +func CreateClusterClient(config *RedisClusterClientConfiguration) (*RedisClusterClient, error) { + connPtr, err := createClient(config) + if err != nil { + return nil, err + } + + return &RedisClusterClient{baseClient{connPtr}}, nil +} diff --git a/go/api/errors.go b/go/api/errors.go new file mode 100644 index 0000000000..97ef710926 --- /dev/null +++ b/go/api/errors.go @@ -0,0 +1,51 @@ +// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// #cgo LDFLAGS: -L../target/release -lglide_rs +// #include "../lib.h" +import "C" + +type GlideError struct { + msg string +} + +func (e GlideError) Error() string { return e.msg } + +type RequestError struct { + msg string +} + +func (e *RequestError) Error() string { return e.msg } + +type ExecAbortError struct { + msg string +} + +func (e *ExecAbortError) Error() string { return e.msg } + +type TimeoutError struct { + msg string +} + +func (e *TimeoutError) Error() string { return e.msg } + +type DisconnectError struct { + msg string +} + +func (e *DisconnectError) Error() string { return e.msg } + +func goError(cErrorType C.RequestErrorType, cErrorMessage *C.char) error { + msg := C.GoString(cErrorMessage) + switch cErrorType { + case C.ExecAbort: + return &ExecAbortError{msg} + case C.Timeout: + return &TimeoutError{msg} + case C.Disconnect: + return &DisconnectError{msg} + default: + return &RequestError{msg} + } +} diff --git a/go/api/requests.go b/go/api/requests.go new file mode 100644 index 0000000000..bfbc71e5ad --- /dev/null +++ b/go/api/requests.go @@ -0,0 +1,99 @@ +// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// #cgo LDFLAGS: -L../target/release -lglide_rs +// #include "../lib.h" +import "C" + +type payload struct { + value *string + error error +} + +type RequestType uint32 + +const ( + _ = iota + customCommand RequestType = iota + getString + setString + ping + info + del + selectDB + configGet + configSet + configResetStat + configRewrite + clientGetName + clientGetRedir + clientId + clientInfo + clientKill + clientList + clientNoEvict + clientNoTouch + clientPause + clientReply + clientSetInfo + clientSetName + clientUnblock + clientUnpause + expire + hashSet + hashGet + hashDel + hashExists + mGet + mSet + incr + incrBy + decr + incrByFloat + decrBy + hashGetAll + hashMSet + hashMGet + hashIncrBy + hashIncrByFloat + lPush + lPop + rPush + rPop + lLen + lRem + lRange + lTrim + sAdd + sRem + sMembers + sCard + pExpireAt + pExpire + expireAt + exists + unlink + ttl + zAdd + zRem + zRange + zCard + zCount + zIncrBy + zScore + keyType + hLen + echo + zPopMin + strlen + lIndex + zPopMax + xRead + xAdd + xReadGroup + xAck + xTrim + xGroupCreate + xGroupDestroy +) diff --git a/go/api/standalone_client.go b/go/api/standalone_client.go new file mode 100644 index 0000000000..01193f97ca --- /dev/null +++ b/go/api/standalone_client.go @@ -0,0 +1,22 @@ +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +package api + +import "C" + +// RedisClient is a client used for connection to standalone Redis servers. +type RedisClient struct { + baseClient +} + +// CreateClient creates a Redis client in standalone mode using the given [RedisClientConfiguration]. +func CreateClient(config *RedisClientConfiguration) (*RedisClient, error) { + connPtr, err := createClient(config) + if err != nil { + return nil, err + } + + return &RedisClient{baseClient{connPtr}}, nil +} diff --git a/go/glide/glide.go b/go/glide/glide.go new file mode 100644 index 0000000000..261b7bb1f3 --- /dev/null +++ b/go/glide/glide.go @@ -0,0 +1,75 @@ +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + package glide + +/* +#cgo LDFLAGS: -L../target/release -lglide_rs +#include "../lib.h" + +void successCallback(uintptr_t channelPtr, char *message); +void failureCallback(uintptr_t channelPtr, char *errMessage, RequestErrorType errType); +*/ +import "C" + +import ( + "fmt" + "github.com/aws/glide-for-redis/go/glide/protobuf" + "google.golang.org/protobuf/proto" + "unsafe" +) + +type GlideRedisClient struct { + coreClient unsafe.Pointer +} + +type payload struct { + value string + errMessage error +} + +type RequestType uint32 + +type ErrorType uint32 + +const ( + ClosingError = iota + RequestError + TimeoutError + ExecAbortError + ConnectionError +) + +//export successCallback +func successCallback(channelPtr C.uintptr_t, message *C.char) { + // TODO: Implement when we implement the command logic +} + +//export failureCallback +func failureCallback(channelPtr C.uintptr_t, errMessage *C.char, errType C.RequestErrorType) { + // TODO: Implement when we implement the command logic +} + +func (glideRedisClient *GlideRedisClient) ConnectToRedis(request *protobuf.ConnectionRequest) error { + marshalledRequest, err := proto.Marshal(request) + if err != nil { + return fmt.Errorf("Failed to encode connection request: %v", err) + } + byteCount := len(marshalledRequest) + requestBytes := C.CBytes(marshalledRequest) + response := (*C.struct_ConnectionResponse)(C.create_client_using_protobuf((*C.uchar)(requestBytes), C.uintptr_t(byteCount), (C.SuccessCallback)(unsafe.Pointer(C.successCallback)), (C.FailureCallback)(unsafe.Pointer(C.failureCallback)))) + defer C.free_connection_response(response) + if response.error_message != nil { + return fmt.Errorf(C.GoString(response.error_message)) + } + glideRedisClient.coreClient = response.conn_ptr + return nil +} + +func (glideRedisClient *GlideRedisClient) CloseClient() error { + if glideRedisClient.coreClient == nil { + return fmt.Errorf("Cannot close glide client before it has been created.") + } + C.close_client(glideRedisClient.coreClient) + return nil +} diff --git a/go/lib.h b/go/lib.h new file mode 100644 index 0000000000..b9f969c4c1 --- /dev/null +++ b/go/lib.h @@ -0,0 +1,93 @@ +/* + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +typedef enum RequestErrorType { + Unspecified = 0, + ExecAbort = 1, + Timeout = 2, + Disconnect = 3, +} RequestErrorType; + +/** + * The connection response. + * + * It contains either a connection or an error. It is represented as a struct instead of an enum for ease of use in the wrapper language. + * + * This struct should be freed using `free_connection_response` to avoid memory leaks. + */ +typedef struct ConnectionResponse { + const void *conn_ptr; + const char *error_message; + RequestErrorType error_type; +} ConnectionResponse; + +/** + * Success callback that is called when a Redis command succeeds. + */ +typedef void (*SuccessCallback)(uintptr_t channel_address, const char *message); + +/** + * Failure callback that is called when a Redis command fails. + * + * `error` should be manually freed by calling `free_error` after this callback is invoked, otherwise a memory leak will occur. + */ +typedef void (*FailureCallback)(uintptr_t channel_address, + const char *error_message, + RequestErrorType error_type); + +/** + * Creates a new client with the given configuration. The success callback needs to copy the given string synchronously, since it will be dropped by Rust once the callback returns. All callbacks should be offloaded to separate threads in order not to exhaust the client's thread pool. + * + * The returned `ConnectionResponse` should be manually freed by calling `free_connection_response`, otherwise a memory leak will occur. It should be freed whether or not an error occurs. + * + * # Safety + * + * * `connection_request_bytes` must point to `connection_request_len` consecutive properly initialized bytes. + * * `connection_request_len` must not be greater than `isize::MAX`. See the safety documentation of [`std::slice::from_raw_parts`](https://doc.rust-lang.org/std/slice/fn.from_raw_parts.html). + */ +const struct ConnectionResponse *create_client_using_protobuf(const uint8_t *connection_request_bytes, + uintptr_t connection_request_len, + SuccessCallback success_callback, + FailureCallback failure_callback); + +/** + * Closes the given client, deallocating it from the heap. + * + * # Safety + * + * * `client_ptr` must be able to be safely casted to a valid `Box` via `Box::from_raw`. See the safety documentation of [`std::boxed::Box::from_raw`](https://doc.rust-lang.org/std/boxed/struct.Box.html#method.from_raw). + * * `client_ptr` must not be null. + */ +void close_client(const void *client_ptr); + +/** + * Deallocates a `ConnectionResponse`. + * + * This function also frees the contained error using `free_error`. + * + * # Safety + * + * * `connection_response_ptr` must be able to be safely casted to a valid `Box` via `Box::from_raw`. See the safety documentation of [`std::boxed::Box::from_raw`](https://doc.rust-lang.org/std/boxed/struct.Box.html#method.from_raw). + * * `connection_response_ptr` must not be null. + * * The contained `error_message` must be able to be safely casted to a valid `CString` via `CString::from_raw`. See the safety documentation of [`std::ffi::CString::from_raw`](https://doc.rust-lang.org/std/ffi/struct.CString.html#method.from_raw). + */ +void free_connection_response(const struct ConnectionResponse *connection_response_ptr); + +/** + * Deallocates an error message `CString`. + * + * # Safety + * + * * `error_msg_ptr` must be able to be safely casted to a valid `CString` via `CString::from_raw`. See the safety documentation of [`std::ffi::CString::from_raw`](https://doc.rust-lang.org/std/ffi/struct.CString.html#method.from_raw). + * * `error_msg_ptr` must not be null. + */ +void free_error(const char *error_msg_ptr); + +// TODO doc +void command(void * client, uintptr_t callback_id, uint32_t request_type, uintptr_t arg_count, const char * const * args); diff --git a/go/src/lib.rs b/go/src/lib.rs index 26b1dacc35..02add3fddc 100644 --- a/go/src/lib.rs +++ b/go/src/lib.rs @@ -1,9 +1,6 @@ /** * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -use std::ffi::c_void; - -#[no_mangle] -pub extern "C" fn create_connection() -> *const c_void { - todo!() -} +// rustc blames empty file or file with a comment only +#[allow(unused_imports)] +use glide_core;