From a425f0bfd4a8a84d89ce69827f9e94c0b3096eb2 Mon Sep 17 00:00:00 2001 From: k33g Date: Mon, 8 May 2023 16:29:11 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=A6=20updates=20HDK=20for=20v0.0.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + Taskfile.yml | 3 +- capsule.dk.go | 53 ++++--- docs/getting-started.md | 17 --- docs/index.md | 1 + go.mod | 7 +- go.sum | 6 + go.work | 1 + go.work.sum | 8 +- hostfunc.envvar.go | 8 +- hostfunc.fileread.go | 14 +- hostfunc.filewrite.go | 16 ++- hostfunc.http.go | 16 ++- hostfunc.log.go | 7 +- hostfunc.memorycache.go | 31 ++-- hostfunc.print.go | 8 +- hostfunc.redis.go | 275 ++++++++++++++++++++++++++++++++++++ memory.go | 61 ++++++-- runtime.go | 43 +++++- samples/Taskfile.yml | 23 +++ samples/cracker/.gitignore | 1 + samples/cracker/go.mod | 13 ++ samples/cracker/go.sum | 10 ++ samples/cracker/main.go | 107 ++++++++++++++ samples/simple-hello/go.mod | 2 +- samples/simple-talk/go.mod | 2 +- samples/simple/go.mod | 2 +- 27 files changed, 646 insertions(+), 90 deletions(-) create mode 100644 hostfunc.redis.go create mode 100644 samples/cracker/.gitignore create mode 100644 samples/cracker/go.mod create mode 100644 samples/cracker/go.sum create mode 100644 samples/cracker/main.go diff --git a/.gitignore b/.gitignore index 0af9b08..812e525 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ host simple-host simple-hello-host simple-talk-host +cracke/cracker *.wasm site diff --git a/Taskfile.yml b/Taskfile.yml index e50b606..b5b8225 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -21,7 +21,8 @@ tasks: release: env: #TAG: "v0.0.1" - TAG: "v0.0.2" # next release + TAG: "v0.0.2" + #TAG: "v0.0.3" # next release cmds: - echo "📦 Generating release..." - git add . diff --git a/capsule.dk.go b/capsule.dk.go index d9be370..e8e4b38 100644 --- a/capsule.dk.go +++ b/capsule.dk.go @@ -10,32 +10,28 @@ import ( const isFailure = rune('F') const isSuccess = rune('S') -/* -func main() { - panic("not implemented") -} - -func success(buffer []byte) uint64 { - return copyBufferToMemory(append([]byte(string(isSuccess)), buffer...)) -} - -func failure(buffer []byte) uint64 { - return copyBufferToMemory(append([]byte(string(isFailure)), buffer...)) -} -*/ - +// success appends the isSuccess byte to the beginning of the input buffer and returns the result. +// +// buffer: byte slice to append isSuccess byte to. +// []byte: byte slice with the appended isSuccess byte. func success(buffer []byte) []byte { return append([]byte(string(isSuccess)), buffer...) } +// failure appends a string "isFailure" to the given byte slice buffer and returns the new slice. +// +// buffer: the byte slice to which "isFailure" is appended. +// Returns the new byte slice with the string "isFailure" appended to it. func failure(buffer []byte) []byte { return append([]byte(string(isFailure)), buffer...) } - - - -// Result function +// Result returns the data without the first byte if the first byte is isSuccess. +// Otherwise, it returns nil and an error with the data starting from the second byte. +// +// data: A byte slice containing the data to check. +// []byte: The data without the first byte if the first byte is isSuccess. +// error: If the first byte is not isSuccess, it returns an error with the data starting from the second byte. func Result(data []byte,) ([]byte, error) { if data[0] == byte(isSuccess) { return data[1:], nil @@ -43,19 +39,30 @@ func Result(data []byte,) ([]byte, error) { return nil, errors.New(string(data[1:])) } -// GetHandle returns the handle function +// GetHandle returns an exported function named "callHandle" from the given module. +// +// mod: The module to retrieve the function from. +// +// Returns: An exported function with the name "callHandle". func GetHandle(mod api.Module) api.Function { return mod.ExportedFunction("callHandle") } -// GetHandleJSON returns the handle function +// GetHandleJSON returns the exported "callHandleJSON" function from the given module. +// +// mod: the module to retrieve the function from. +// +// returns: the exported "callHandleJSON" function. func GetHandleJSON(mod api.Module) api.Function { return mod.ExportedFunction("callHandleJSON") } -// GetHandleHTTP returns the handle function +// GetHandleHTTP returns the exported 'callHandleHTTP' function from a given module. +// +// mod: The module containing the exported function. +// +// returns: +// - api.Function: the exported 'callHandleHTTP' function. func GetHandleHTTP(mod api.Module) api.Function { return mod.ExportedFunction("callHandleHTTP") } - -// TODO: handle the other handles diff --git a/docs/getting-started.md b/docs/getting-started.md index ccabf92..62de82d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -4,20 +4,3 @@ ## Create a Capsule application -Create a directory `say-hello` - -```bash -mkdir say-hello -cd say-hello -``` - -Initialize a new project in `say-hello`: - -```bash -go mod init say-hello -``` - -Install the Capsule **HDK** dependencies: -```bash -go get github.com/bots-garden/capsule-host-sdk -``` diff --git a/docs/index.md b/docs/index.md index 679d57f..e3909d4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,7 @@ # Capsule Host SDK !!! info "What's new?" + - `v0.0.2`: ✨ Redis support - `v0.0.1`: 🎉 first release ## What is the Capsule Host SDK alias **Capsule HDK**? diff --git a/go.mod b/go.mod index 4df2e12..853c9b9 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,9 @@ require ( github.com/tetratelabs/wazero v1.1.0 ) -require golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/redis/go-redis/v9 v9.0.4 // indirect + golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect +) diff --git a/go.sum b/go.sum index 283e091..9e532a7 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= +github.com/redis/go-redis/v9 v9.0.4 h1:FC82T+CHJ/Q/PdyLW++GeCO+Ol59Y4T7R4jbgjvktgc= +github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/tetratelabs/wazero v1.1.0 h1:EByoAhC+QcYpwSZJSs/aV0uokxPwBgKxfiokSUwAknQ= github.com/tetratelabs/wazero v1.1.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM= diff --git a/go.work b/go.work index 8f0e7aa..6387c67 100644 --- a/go.work +++ b/go.work @@ -4,4 +4,5 @@ use ( ./samples/simple ./samples/simple-hello ./samples/simple-talk + ./samples/cracker ) diff --git a/go.work.sum b/go.work.sum index c5bb68f..307632c 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,5 +1,9 @@ +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/redis/go-redis/v9 v9.0.4 h1:FC82T+CHJ/Q/PdyLW++GeCO+Ol59Y4T7R4jbgjvktgc= +github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/tetratelabs/wazero v1.0.1 h1:xyWBoGyMjYekG3mEQ/W7xm9E05S89kJ/at696d/9yuc= github.com/tetratelabs/wazero v1.0.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= -github.com/tetratelabs/wazero v1.1.0 h1:EByoAhC+QcYpwSZJSs/aV0uokxPwBgKxfiokSUwAknQ= -github.com/tetratelabs/wazero v1.1.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= diff --git a/hostfunc.envvar.go b/hostfunc.envvar.go index 6178d85..a1eb3fb 100644 --- a/hostfunc.envvar.go +++ b/hostfunc.envvar.go @@ -1,6 +1,5 @@ package capsule -// this hostfunction is a template for the other host functions import ( "context" "log" @@ -10,7 +9,12 @@ import ( "github.com/tetratelabs/wazero/api" ) -// DefineHostFuncGetEnv defines a host function allowing the wasm guest to get an environment variable by name +// DefineHostFuncGetEnv defines a new host function to get the environment variable value. +// +// Parameters: +// - builder: the HostModuleBuilder to add the function to. +// +// Returns: nothing. func DefineHostFuncGetEnv(builder wazero.HostModuleBuilder) { builder.NewFunctionBuilder(). WithGoModuleFunction(getEnv, diff --git a/hostfunc.fileread.go b/hostfunc.fileread.go index 4fb3b9a..9b7d36b 100644 --- a/hostfunc.fileread.go +++ b/hostfunc.fileread.go @@ -1,6 +1,5 @@ package capsule -// this hostfunction is a template for the other host functions import ( "context" "log" @@ -10,7 +9,14 @@ import ( "github.com/tetratelabs/wazero/api" ) -// DefineHostFuncReadFile defines a host function +// DefineHostFuncReadFile defines a function that reads a file from the host file system +// and returns its content as a string. The function takes in four parameters: +// - filePath: the pointer to the string representing the file path +// - filePathLen: the length of the file path string +// - returned: a pointer to the string where the file content will be stored +// - returnedLen: the length of the returned string +// +// The function returns an integer representing whether the operation was successful. func DefineHostFuncReadFile(builder wazero.HostModuleBuilder) { builder.NewFunctionBuilder(). WithGoModuleFunction(readFile, @@ -57,7 +63,3 @@ var readFile = api.GoModuleFunc(func(ctx context.Context, module api.Module, par params[0] = 0 }) - -/* Documentation: - -*/ diff --git a/hostfunc.filewrite.go b/hostfunc.filewrite.go index eba6b97..4ba91c9 100644 --- a/hostfunc.filewrite.go +++ b/hostfunc.filewrite.go @@ -1,6 +1,5 @@ package capsule -// this hostfunction is a template for the other host functions import ( "context" "log" @@ -10,7 +9,15 @@ import ( "github.com/tetratelabs/wazero/api" ) -// DefineHostFuncWriteFile defines a host function +// DefineHostFuncWriteFile creates a new function called hostWriteFile in the +// HostModuleBuilder. It accepts the following parameters: +// - filePath (int32): position +// - filePath (int32): length +// - content (int32): position +// - content (int32): length +// - returned (int32): position +// - returned (int32): length +// The function returns an int32. func DefineHostFuncWriteFile(builder wazero.HostModuleBuilder) { builder.NewFunctionBuilder(). WithGoModuleFunction(writeFile, @@ -66,8 +73,3 @@ var writeFile = api.GoModuleFunc(func(ctx context.Context, module api.Module, pa params[0] = 0 }) - -/* Documentation: -! concurrent are not managed -? don't use it with the capsule-http runner - */ diff --git a/hostfunc.http.go b/hostfunc.http.go index 0101b6e..7767033 100644 --- a/hostfunc.http.go +++ b/hostfunc.http.go @@ -25,7 +25,13 @@ type request struct { Headers map[string]string `json:"Headers"` } -// DefineHostFuncHTTP defines a host function +// DefineHostFuncHTTP defines the host module function for handling HTTP requests. +// +// Parameter(s): +// builder: the wazero.HostModuleBuilder used to define the function. +// +// Return(s): +// None. func DefineHostFuncHTTP(builder wazero.HostModuleBuilder) { builder.NewFunctionBuilder(). WithGoModuleFunction(http, @@ -170,6 +176,10 @@ var http = api.GoModuleFunc(func(ctx context.Context, module api.Module, params }) +// buildResponseJSONString takes a resty.Response pointer as input and returns a JSON string and an error. +// The function builds a JSON string of the response headers and response body. If the response is in JSON format, +// the function includes the JSON body in the JSON string; otherwise, the function includes the text body in the JSON +// string with double quotes. The function also includes the status code in the JSON string. func buildResponseJSONString(resp *resty.Response) (string, error) { // build headers JSON string // ! ATTENTION resp.Header() return a map[string]string[] (instead of map[string]string) @@ -205,7 +215,3 @@ func buildResponseJSONString(resp *resty.Response) (string, error) { return jsonHTTPResponse, err } - -/* Documentation: - - */ diff --git a/hostfunc.log.go b/hostfunc.log.go index f4e55da..8c15e02 100644 --- a/hostfunc.log.go +++ b/hostfunc.log.go @@ -30,8 +30,11 @@ var logString = api.GoModuleFunc(func(ctx context.Context, module api.Module, pa params[0] = 0 // return 0 }) - -// DefineHostFuncLog defines a host function +// DefineHostFuncLog defines and exports a host module function called hostLogString. +// This function takes two parameters: +// - string position (i32) +// - string length (i32) +// It returns an i32 value. func DefineHostFuncLog(builder wazero.HostModuleBuilder) { // hostLogString builder.NewFunctionBuilder(). diff --git a/hostfunc.memorycache.go b/hostfunc.memorycache.go index 3fe474b..3ae66ed 100644 --- a/hostfunc.memorycache.go +++ b/hostfunc.memorycache.go @@ -23,7 +23,15 @@ import ( var memCache sync.Map -// DefineHostFuncCacheSet defines a host function +// DefineHostFuncCacheSet defines a new Go module function for setting values in +// the cache. It takes in 6 parameters: +// - key position (int32) +// - key length (int32) +// - value string position (int32) +// - value string length (int32) +// - returned position (int32) +// - returned length (int32) +// It returns an int32 value. func DefineHostFuncCacheSet(builder wazero.HostModuleBuilder) { builder.NewFunctionBuilder(). WithGoModuleFunction(cacheSet, @@ -83,7 +91,11 @@ var cacheSet = api.GoModuleFunc(func(ctx context.Context, module api.Module, par }) -// DefineHostFuncCacheGet defines a host function +// DefineHostFuncCacheGet defines the Go function that calls the cacheGet function +// to get the value of a given key. The function takes in four parameters: the +// position of the key, the length of the key, the position of the returned value, +// and the length of the returned value. It returns an integer that represents +// the success or failure of the function call. func DefineHostFuncCacheGet(builder wazero.HostModuleBuilder) { builder.NewFunctionBuilder(). WithGoModuleFunction(cacheGet, @@ -136,8 +148,12 @@ var cacheGet = api.GoModuleFunc(func(ctx context.Context, module api.Module, par }) - -// DefineHostFuncCacheDel defines a host function +// DefineHostFuncCacheDel defines a Go function that deletes a cache entry. +// +// Parameters: +// - builder: a wazero.HostModuleBuilder object. +// +// Returns: nothing. func DefineHostFuncCacheDel(builder wazero.HostModuleBuilder) { builder.NewFunctionBuilder(). WithGoModuleFunction(cacheDel, @@ -186,10 +202,9 @@ var cacheDel = api.GoModuleFunc(func(ctx context.Context, module api.Module, par }) - - - -// DefineHostFuncCacheKeys defines a host function +// DefineHostFuncCacheKeys defines the host function hostCacheKeys which takes in +// filter position, filter length, returned position, and returned length as +// parameters of type i32 and returns an i32. func DefineHostFuncCacheKeys(builder wazero.HostModuleBuilder) { builder.NewFunctionBuilder(). WithGoModuleFunction(cacheKeys, diff --git a/hostfunc.print.go b/hostfunc.print.go index 750b73d..ff7128c 100644 --- a/hostfunc.print.go +++ b/hostfunc.print.go @@ -28,7 +28,12 @@ var printString = api.GoModuleFunc(func(ctx context.Context, module api.Module, }) -// DefineHostFuncPrint defines a host function +// DefineHostFuncPrint defines the hostPrintString function which takes in a +// string position and string length as parameters, and returns an integer. +// +// builder: The HostModuleBuilder object. +// +// Returns: None. func DefineHostFuncPrint(builder wazero.HostModuleBuilder) { // hostPrintString builder.NewFunctionBuilder(). @@ -40,3 +45,4 @@ func DefineHostFuncPrint(builder wazero.HostModuleBuilder) { []api.ValueType{api.ValueTypeI32}). Export("hostPrintString") } +// DefineHostFuncPrint defines a host function diff --git a/hostfunc.redis.go b/hostfunc.redis.go new file mode 100644 index 0000000..ea47154 --- /dev/null +++ b/hostfunc.redis.go @@ -0,0 +1,275 @@ +package capsule + +import ( + "context" + "encoding/json" + "log" + "strconv" + + "github.com/bots-garden/capsule-host-sdk/helpers" + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + + "github.com/redis/go-redis/v9" +) + +var redisDb *redis.Client + +// InitRedisClient initializes a Redis client instance if it is not already initialized. +func InitRedisClient() { + if redisDb == nil { + defaultDb, _ := strconv.Atoi(helpers.GetEnv("REDIS_DEFAULTDB", "0")) + redisDb = redis.NewClient(&redis.Options{ + Addr: helpers.GetEnv("REDIS_ADDR", "localhost:6379"), + Password: helpers.GetEnv("REDIS_PWD", ""), // no password set + DB: defaultDb, // use default DB + }) + } +} + +// getRedisClient returns a pointer to a Redis client. +// +// This function takes no parameters. +// It returns a pointer to a Redis client. +func getRedisClient() *redis.Client { + return redisDb +} + +// DefineHostFuncRedisSet defines a Go function that sets a value in Redis. +// +// It takes in the key and value string positions and lengths as well as the +// positions and lengths of the returned value. It returns an integer value. +func DefineHostFuncRedisSet(builder wazero.HostModuleBuilder) { + builder.NewFunctionBuilder(). + WithGoModuleFunction(redisSet, + []api.ValueType{ + api.ValueTypeI32, // key position + api.ValueTypeI32, // key length + api.ValueTypeI32, // value string position + api.ValueTypeI32, // value string length + api.ValueTypeI32, // returned position + api.ValueTypeI32, // returned length + }, + []api.ValueType{api.ValueTypeI32}). + Export("hostRedisSet") +} + +// redisSet : host function called by the wasm function +// and then returning data to the wasm module +var redisSet = api.GoModuleFunc(func(ctx context.Context, module api.Module, params []uint64) { + + // read the value of the arguments of the function + keyPosition := uint32(params[0]) + keyLength := uint32(params[1]) + + bufferKey, err := ReadBytesParameterFromMemory(module, keyPosition, keyLength) + if err != nil { + log.Panicf("Error (bufferKey): ReadBytesParameterFromMemory(%d, %d) out of range", keyPosition, keyLength) + } + + stringValuePosition := uint32(params[2]) + stringValueLength := uint32(params[3]) + + bufferStringValue, err := ReadBytesParameterFromMemory(module, stringValuePosition, stringValueLength) + if err != nil { + log.Panicf("Error (bufferStringValue): ReadBytesParameterFromMemory(%d, %d) out of range", stringValuePosition, stringValueLength) + } + + // Execute the host function with the arguments and return a value + var resultFromHost []byte + + // start the host work (using Redis client) + InitRedisClient() // initialize the redis client only if it does not exist + err = getRedisClient().Set(ctx, string(bufferKey), string(bufferStringValue), 0).Err() // TODO: check if []byte is ok for the value + if err != nil { + resultFromHost = failure([]byte(err.Error())) + } else { + resultFromHost = success(bufferKey) + } + // end of the host work + + // return the result value (using the return buffer) + positionReturnBuffer := uint32(params[4]) + lengthReturnBuffer := uint32(params[5]) + + _, errReturn := ReturnBytesToMemory(ctx, module, positionReturnBuffer, lengthReturnBuffer, resultFromHost) + if errReturn != nil { + log.Panicf("Error: ReturnBytesToMemory(%d, %d) out of range", positionReturnBuffer, lengthReturnBuffer) + } + + params[0] = 0 + +}) + + +// DefineHostFuncRedisGet defines a function that gets a value from Redis cache. +func DefineHostFuncRedisGet(builder wazero.HostModuleBuilder) { + builder.NewFunctionBuilder(). + WithGoModuleFunction(redisGet, + []api.ValueType{ + api.ValueTypeI32, // key position + api.ValueTypeI32, // key length + api.ValueTypeI32, // returned position + api.ValueTypeI32, // returned length + }, + []api.ValueType{api.ValueTypeI32}). + Export("hostCacheGet") +} + +// redisGet : host function called by the wasm function +// and then returning data to the wasm module +var redisGet = api.GoModuleFunc(func(ctx context.Context, module api.Module, params []uint64) { + + // read the value of the arguments of the function + keyPosition := uint32(params[0]) + keyLength := uint32(params[1]) + + bufferKey, err := ReadBytesParameterFromMemory(module, keyPosition, keyLength) + if err != nil { + log.Panicf("Error (bufferKey): ReadBytesParameterFromMemory(%d, %d) out of range", keyPosition, keyLength) + } + + // Execute the host function with the arguments and return a value + var resultFromHost []byte + + // start the host work + result, err := getRedisClient().Get(ctx, string(bufferKey)).Result() + if err != nil { + resultFromHost = failure([]byte(err.Error())) + } else { + resultFromHost = success([]byte(result)) + } + // end of the host work + + // return the result value (using the return buffer) + positionReturnBuffer := uint32(params[2]) + lengthReturnBuffer := uint32(params[3]) + + _, errReturn := ReturnBytesToMemory(ctx, module, positionReturnBuffer, lengthReturnBuffer, resultFromHost) + if errReturn != nil { + log.Panicf("Error: ReturnBytesToMemory(%d, %d) out of range", positionReturnBuffer, lengthReturnBuffer) + } + + params[0] = 0 + +}) + +// DefineHostFuncRedisDel defines a Redis Del operation for the host module builder. +// +// This function takes in a `builder` of type `wazero.HostModuleBuilder` and creates a new +// function builder for Redis Del operation. The function builder is then configured with +// parameters and exports the function with name "hostCacheDel". +func DefineHostFuncRedisDel(builder wazero.HostModuleBuilder) { + builder.NewFunctionBuilder(). + WithGoModuleFunction(redisDel, + []api.ValueType{ + api.ValueTypeI32, // key position + api.ValueTypeI32, // key length + api.ValueTypeI32, // returned position + api.ValueTypeI32, // returned length + }, + []api.ValueType{api.ValueTypeI32}). + Export("hostCacheDel") +} + +// redisDel : host function called by the wasm function +// and then returning data to the wasm module +var redisDel = api.GoModuleFunc(func(ctx context.Context, module api.Module, params []uint64) { + + // read the value of the arguments of the function + keyPosition := uint32(params[0]) + keyLength := uint32(params[1]) + + bufferKey, err := ReadBytesParameterFromMemory(module, keyPosition, keyLength) + if err != nil { + log.Panicf("Error (bufferKey): ReadBytesParameterFromMemory(%d, %d) out of range", keyPosition, keyLength) + } + + // Execute the host function with the arguments and return a value + var resultFromHost []byte + + // start the host work + _, err = getRedisClient().Del(ctx, string(bufferKey)).Result() + if err != nil { + resultFromHost = failure([]byte(err.Error())) + } else { + resultFromHost = success(bufferKey) + } + // end of the host work + + // return the result value (using the return buffer) + positionReturnBuffer := uint32(params[2]) + lengthReturnBuffer := uint32(params[3]) + + _, errReturn := ReturnBytesToMemory(ctx, module, positionReturnBuffer, lengthReturnBuffer, resultFromHost) + if errReturn != nil { + log.Panicf("Error: ReturnBytesToMemory(%d, %d) out of range", positionReturnBuffer, lengthReturnBuffer) + } + + params[0] = 0 + +}) + +// DefineHostFuncRedisKeys defines a function that exports a host module function +// that retrieves Redis cache keys. It takes in four parameters: filter position, +// filter length, returned position and returned length. It returns an integer. +func DefineHostFuncRedisKeys(builder wazero.HostModuleBuilder) { + builder.NewFunctionBuilder(). + WithGoModuleFunction(cacheKeys, + []api.ValueType{ + api.ValueTypeI32, // filter position + api.ValueTypeI32, // filter length + api.ValueTypeI32, // returned position + api.ValueTypeI32, // returned length + }, + []api.ValueType{api.ValueTypeI32}). + Export("hostCacheKeys") +} + +// redisKeys : host function called by the wasm function +// and then returning data to the wasm module +var redisKeys = api.GoModuleFunc(func(ctx context.Context, module api.Module, params []uint64) { + + // read the value of the arguments of the function + filterPosition := uint32(params[0]) + filterLength := uint32(params[1]) + + bufferFilter, err := ReadBytesParameterFromMemory(module, filterPosition, filterLength) + if err != nil { + log.Panicf("Error (bufferFilter): ReadBytesParameterFromMemory(%d, %d) out of range", filterPosition, filterLength) + } + + // Execute the host function with the arguments and return a value + var resultFromHost []byte + + // start the host work + var keysMap = make(map[string][]string) + + // call the redis KEYS command + keys, err := getRedisClient().Keys(ctx, string(bufferFilter)).Result() + if err != nil { + resultFromHost = failure([]byte(err.Error())) + } else { + keysMap["keys"] = keys + jsonStr, err := json.Marshal(keysMap) + // {"keys":["key1","key2"]} + if err != nil { + resultFromHost = failure(jsonStr) + } else { + resultFromHost = success(jsonStr) + } + } + // end of the host work + + // return the result value (using the return buffer) + positionReturnBuffer := uint32(params[2]) + lengthReturnBuffer := uint32(params[3]) + + _, errReturn := ReturnBytesToMemory(ctx, module, positionReturnBuffer, lengthReturnBuffer, resultFromHost) + if errReturn != nil { + log.Panicf("Error: ReturnBytesToMemory(%d, %d) out of range", positionReturnBuffer, lengthReturnBuffer) + } + + params[0] = 0 + +}) diff --git a/memory.go b/memory.go index e388179..1ee06da 100644 --- a/memory.go +++ b/memory.go @@ -7,7 +7,14 @@ import ( "github.com/tetratelabs/wazero/api" ) -// CopyDataToMemory returns the position and size of the data in memory +// CopyDataToMemory copies data to memory. +// +// ctx: The context for this function. +// mod: The module to copy the data to. +// data: The data to be copied to memory. +// +// uint64, uint64, error: The position of the copied data, the size of the data, +// and an error if one occurs. func CopyDataToMemory(ctx context.Context, mod api.Module, data []byte) (uint64, uint64, error) { // These function are exported by TinyGo malloc := mod.ExportedFunction("malloc") @@ -35,7 +42,10 @@ func CopyDataToMemory(ctx context.Context, mod api.Module, data []byte) (uint64, } -// UnPackPosSize extract the position and size from a unique value +// UnPackPosSize extracts the position and size of the returned value from a given pair. +// +// pair: 64-bit unsigned integer. +// Returns a pair of 32-bit unsigned integers. func UnPackPosSize(pair uint64) (uint32, uint32) { // Extract the position and size of the returned value pos := uint32(pair >> 32) @@ -43,8 +53,16 @@ func UnPackPosSize(pair uint64) (uint32, uint32) { return pos, size } - -// ReadDataFromMemory returns the data in memory +// ReadDataFromMemory reads data from a given position in the memory of a module. +// +// Parameters: +// - mod: the module to read data from. +// - pos: the position in the memory to read from. +// - size: the size of the data to read. +// +// Returns: +// - a byte slice containing the read data. +// - an error if the position or size are out of range of the memory size. func ReadDataFromMemory(mod api.Module, pos uint32, size uint32) ([]byte, error) { // Read the value from the memory bytes, ok := mod.Memory().Read(pos, size) @@ -54,7 +72,8 @@ func ReadDataFromMemory(mod api.Module, pos uint32, size uint32) ([]byte, error) return bytes, nil } -// ReadBytesFromMemory returns the data in memory +// ReadBytesFromMemory reads a sequence of bytes from the given module's memory starting from pos and +// with a length of size. It returns the bytes read and any error encountered. func ReadBytesFromMemory(mod api.Module, pos uint32, size uint32) ([]byte, error) { data, err := ReadDataFromMemory(mod, pos, size) if err != nil { @@ -63,10 +82,22 @@ func ReadBytesFromMemory(mod api.Module, pos uint32, size uint32) ([]byte, error result, err := Result(data) return result, err } +// ReadBytesFromMemory returns the data in memory + + //! When using host function -// ReadBytesParameterFromMemory → read the parameter(s) sent by the WASM guest +// ReadBytesParameterFromMemory reads a slice of bytes from the given position +// in memory of the provided module. Returns the slice of bytes and an error if +// the read operation failed due to the specified position being out of range. +// +// mod: The module from which to read memory. +// pos: The starting position to read from. +// size: The number of bytes to read. +// +// Returns: A slice of bytes read from memory and an error if the read operation +// failed. func ReadBytesParameterFromMemory(mod api.Module, pos uint32, size uint32) ([]byte, error) { buff, ok := mod.Memory().Read(pos, size) if !ok { @@ -75,7 +106,19 @@ func ReadBytesParameterFromMemory(mod api.Module, pos uint32, size uint32) ([]by return buff, nil } -// ReturnBytesToMemory → return data to the WASM guest +// ReturnBytesToMemory writes data from the host to a buffer in the module's memory +// and updates the buffer information in the module. It returns a boolean value +// indicating whether the write was successful and an error if any. +// +// ctx: context required for the operation. +// mod: the module where the buffer is. +// positionReturnBuffer: the position in memory where the buffer's position will be written. +// lengthReturnBuffer: the position in memory where the buffer's length will be written. +// dataFromHost: the data to be written to the buffer. +// +// Returns: +// - a boolean indicating whether the write was successful. +// - an error if any. func ReturnBytesToMemory(ctx context.Context, mod api.Module, positionReturnBuffer uint32, lengthReturnBuffer uint32, dataFromHost []byte) (bool, error) { dataFromHostLength := len(dataFromHost) // This is a wasm function defined in the capsule-module-sdk @@ -91,8 +134,6 @@ func ReturnBytesToMemory(ctx context.Context, mod api.Module, positionReturnBuff return mod.Memory().Write(allocatedPosition, dataFromHost), nil } +// ReturnBytesToMemory → return data to the WASM guest -/* Documentation: - -*/ diff --git a/runtime.go b/runtime.go index 502e308..54e8c4d 100644 --- a/runtime.go +++ b/runtime.go @@ -4,10 +4,12 @@ import ( "context" "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" ) -// GetBuilder returns the builder +// GetBuilder returns a new instance of the HostModuleBuilder +// configured with the default host functions func GetBuilder(runtime wazero.Runtime) wazero.HostModuleBuilder { builder := runtime.NewHostModuleBuilder("env") @@ -28,7 +30,8 @@ func GetBuilder(runtime wazero.Runtime) wazero.HostModuleBuilder { return builder } -// GetRuntime returns the runtime +// GetRuntime returns the WebAssembly runtime. +// It takes a context and returns a wazero.Runtime object. func GetRuntime(ctx context.Context) wazero.Runtime { // Create a new WebAssembly Runtime. runtime := wazero.NewRuntime(ctx) @@ -38,3 +41,39 @@ func GetRuntime(ctx context.Context) wazero.Runtime { return runtime } + +// CallHandleFunction calls the given handleFunction with the argument argFunction +// and returns the result. The function uses CopyDataToMemory to copy the argument +// to memory, and UnPackPosSize to unpack the result. Returns a byte slice and an +// error. +// +// ctx: The context.Context +// mod: The api.Module +// handleFunction: The api.Function to be called +// argFunction: The argument to the function +// +// Returns ([]byte, error). +func CallHandleFunction(ctx context.Context, mod api.Module, handleFunction api.Function, argFunction []byte) ([]byte, error) { + + // send argument to the function + pos, size, err := CopyDataToMemory(ctx, mod, argFunction) + if err != nil { + return nil, err + } + + // Now, we can call "callHandle" with the position and the size of "Bob Morane" + // the result type is []uint64 + result, err := handleFunction.Call(ctx, pos, size) + if err != nil { + return nil, err + } + // read the result of the function + resultMemoryPosition, resultSize := UnPackPosSize(result[0]) + + bufferResult, err := ReadDataFromMemory(mod, resultMemoryPosition, resultSize) + if err != nil { + return nil, err + } + + return Result(bufferResult) +} diff --git a/samples/Taskfile.yml b/samples/Taskfile.yml index 5915406..f75f05a 100644 --- a/samples/Taskfile.yml +++ b/samples/Taskfile.yml @@ -104,3 +104,26 @@ tasks: echo "🚀 Testing simple-talk-host..." cd simple-talk ./simple-talk-host + + # Build the cracker host + # It's a simple http server + build-cracker-host: + cmds: + - | + echo "📦 Building cracker host..." + cd cracker + go build -o cracker + ls -lh cracker + + test-cracker-host: + cmds: + - | + echo "🚀 Testing cracker host..." + cd cracker + ./cracker ../../../capsule-module-sdk/samples/say-hello/say-hello.wasm 8080 & + sleep 1 + - | + curl -X POST http://localhost:8080 \ + -H 'Content-Type: text/plain; charset=utf-8' \ + -d "Bob Morane 🥰" + - killport 8080 \ No newline at end of file diff --git a/samples/cracker/.gitignore b/samples/cracker/.gitignore new file mode 100644 index 0000000..ba90b2e --- /dev/null +++ b/samples/cracker/.gitignore @@ -0,0 +1 @@ +cracker diff --git a/samples/cracker/go.mod b/samples/cracker/go.mod new file mode 100644 index 0000000..79e6032 --- /dev/null +++ b/samples/cracker/go.mod @@ -0,0 +1,13 @@ +module cracker + +go 1.20 + +require ( + github.com/go-resty/resty/v2 v2.7.0 // indirect + github.com/tetratelabs/wazero v1.1.0 + golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect +) + +require github.com/bots-garden/capsule-host-sdk v0.0.2 + +replace github.com/bots-garden/capsule-host-sdk => ../.. diff --git a/samples/cracker/go.sum b/samples/cracker/go.sum new file mode 100644 index 0000000..27017d3 --- /dev/null +++ b/samples/cracker/go.sum @@ -0,0 +1,10 @@ +github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= +github.com/tetratelabs/wazero v1.1.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/samples/cracker/main.go b/samples/cracker/main.go new file mode 100644 index 0000000..758ad93 --- /dev/null +++ b/samples/cracker/main.go @@ -0,0 +1,107 @@ +// Package main → a simple http server +package main + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + + "github.com/bots-garden/capsule-host-sdk" + "github.com/bots-garden/capsule-host-sdk/helpers" + + "github.com/tetratelabs/wazero" +) + + +// callWASMFunction is a Go function that instantiates a WebAssembly module from a file +// and calls a function exported by the Capsule plugin. It takes in an http.ResponseWriter +// and an http.Request as parameters. The function reads the request body and passes it +// to the exported function. It writes the result to the http.ResponseWriter. +// +// Parameters: +// - w (http.ResponseWriter): A ResponseWriter interface that provides a way to +// construct an HTTP response. +// - req (*http.Request): An HTTP request received by the server. +// +// Returns: +// This function does not return any values. +func callWASMFunction(w http.ResponseWriter, req *http.Request) { + + mod, err := runtime.Instantiate(ctx, wasmFile) + if err != nil { + fmt.Fprintf(w, err.Error()+"\n") + } + // Get the reference to the WebAssembly function: "callHandle" + // callHandle is exported by the Capsule plugin + handleFunction := capsule.GetHandle(mod) + + body, err := ioutil.ReadAll(req.Body) + if err != nil { + fmt.Fprintf(w, err.Error()+"\n") + } + + result, err := capsule.CallHandleFunction(ctx, mod, handleFunction, body) + + if err != nil { + fmt.Fprintf(w, err.Error()+"\n") + } else { + fmt.Fprintf(w, string(result)+"\n") + } + +} + +var wasmFile []byte +var runtime wazero.Runtime +var ctx context.Context + +// main initializes a WebAssembly Runtime and loads a WebAssembly module to be served as a HTTP request. +// +// It takes no parameters, and returns no values. +func main() { + + ctx = context.Background() + + // Create a new WebAssembly Runtime. + runtime = capsule.GetRuntime(ctx) + + // START: host functions + // Get the builder and load the default host functions + builder := capsule.GetBuilder(runtime) + + // Add your host functions here + // 🏠 + // End of of you hostfunction + + // Instantiate builder and default host functions + _, err := builder.Instantiate(ctx) + if err != nil { + log.Println(err) + os.Exit(1) + } + // END: host functions + + // This closes everything this Runtime created. + defer runtime.Close(ctx) + + // Load the WebAssembly module + args := os.Args[1:] + wasmFilePath := args[0] + httpPort := args[1] + + wasmFile, err = helpers.LoadWasmFile(wasmFilePath) + if err != nil { + log.Println(err) + os.Exit(1) + } + + // Registering the hello handler + http.HandleFunc("/", callWASMFunction) + + fmt.Println("Cracker is listening on", httpPort) + + // Listening on port 8080 + http.ListenAndServe(":"+httpPort, nil) +} diff --git a/samples/simple-hello/go.mod b/samples/simple-hello/go.mod index a0f2846..bff4439 100644 --- a/samples/simple-hello/go.mod +++ b/samples/simple-hello/go.mod @@ -4,7 +4,7 @@ go 1.20 require github.com/tetratelabs/wazero v1.1.0 -require github.com/bots-garden/capsule-host-sdk v0.0.0 +require github.com/bots-garden/capsule-host-sdk v0.0.2 require ( github.com/go-resty/resty/v2 v2.7.0 // indirect diff --git a/samples/simple-talk/go.mod b/samples/simple-talk/go.mod index c77bc88..c1e71d2 100644 --- a/samples/simple-talk/go.mod +++ b/samples/simple-talk/go.mod @@ -8,6 +8,6 @@ require ( golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect ) -require github.com/bots-garden/capsule-host-sdk v0.0.0 +require github.com/bots-garden/capsule-host-sdk v0.0.2 replace github.com/bots-garden/capsule-host-sdk => ../.. diff --git a/samples/simple/go.mod b/samples/simple/go.mod index c4165ab..93091df 100644 --- a/samples/simple/go.mod +++ b/samples/simple/go.mod @@ -4,7 +4,7 @@ go 1.20 require github.com/tetratelabs/wazero v1.1.0 // indirect -require github.com/bots-garden/capsule-host-sdk v0.0.0 +require github.com/bots-garden/capsule-host-sdk v0.0.2 require ( github.com/go-resty/resty/v2 v2.7.0 // indirect