diff --git a/.gitignore b/.gitignore index 3b735ec..528c201 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,11 @@ # Go workspace file go.work + +# Cgo files +*.a +*.h +test + +# MacOS system files +.DS_Store diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d10c954 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +PROJECT_NAME := comandy +BUILD_PATH := build +GOOS := android +GOARCH := arm64 +BUILD_MACHINE := darwin +BUILD_ARCH := x86_64 +NDK_VERSION := 26.3.11579264 +TARGET_SDK := android23 + +CGO_ENABLED := 1 +GO_SRC := $(shell find . -name '*.go') +NDK_TOOLCHAIN := ~/Library/Android/sdk/ndk/$(NDK_VERSION)/toolchains/llvm/prebuilt/$(BUILD_MACHINE)-$(BUILD_ARCH) +CC := $(NDK_TOOLCHAIN)/bin/aarch64-linux-$(TARGET_SDK)-clang +TEST_OUTPUT = '$(shell cd $(BUILD_PATH) && ./test)' +TEST_EXPECTED = '{"code":500,"data":"aW52YWxpZCB1cmwgJyc="}' + +all: shared + +shared: $(GO_SRC) dir + GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=$(CGO_ENABLED) NDK_TOOLCHAIN=$(NDK_TOOLCHAIN) CC=$(CC) go build -buildmode=c-shared -o $(BUILD_PATH)/lib$(PROJECT_NAME).so $(GO_SRC) +test: dir + @GOOS=$(BUILD_MACHINE) CC=cc NDK_TOOLCHAIN="" $(MAKE) -e shared + cc -o $(BUILD_PATH)/test $(BUILD_PATH)/test.c -l$(PROJECT_NAME) -L$(BUILD_PATH) +runtest: test + @if [ $(TEST_OUTPUT) = $(TEST_EXPECTED) ]; then \ + echo "test succeeded."; \ + else \ + echo "test failed, expected:" $(TEST_EXPECTED) "but got:" $(TEST_OUTPUT); \ + fi +dir: + @if [ ! -d "$(BUILD_PATH)" ]; then mkdir $(BUILD_PATH); fi +clean: + @if [ -d "$(BUILD_PATH)" ]; then rm -rf $(BUILD_PATH)/lib$(PROJECT_NAME).*; fi diff --git a/build/test.c b/build/test.c new file mode 100644 index 0000000..921fc89 --- /dev/null +++ b/build/test.c @@ -0,0 +1,8 @@ +#include + +#include "libcomandy.h" + +int main() { + char* msg = request("{}"); + puts(msg); +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1de9e01 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module comandy + +go 1.22.1 + +require github.com/fumiama/terasu v0.0.0-20240414143030-44fae3a81905 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f5e7d94 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/fumiama/terasu v0.0.0-20240414143030-44fae3a81905 h1:PHf84+ujLpFGJbfytrwZT6/D7KojmjFm5Itv6te6WUA= +github.com/fumiama/terasu v0.0.0-20240414143030-44fae3a81905/go.mod h1:BFl0X1+rGJf8bLHl/kO+v05ryHrj/R4kyCrK89NvegA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..0f47732 --- /dev/null +++ b/main.go @@ -0,0 +1,130 @@ +package main + +import "C" + +import ( + "context" + "crypto/tls" + "encoding/base64" + "encoding/json" + "io" + "net" + "net/http" + "reflect" + "strings" + "time" + + "github.com/fumiama/terasu" +) + +func main() {} + +var dialer = net.Dialer{ + Timeout: time.Minute, +} + +var cli = http.Client{ + Transport: &http.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := dialer.DialContext(ctx, "tcp", addr) + if err != nil { + return nil, err + } + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + return terasu.Use(tls.Client(conn, &tls.Config{ + ServerName: host, + InsecureSkipVerify: true, + })), nil + }, + }, +} + +type capsule struct { + C int `json:"code,omitempty"` + M string `json:"method,omitempty"` + U string `json:"url,omitempty"` + H map[string]any `json:"headers,omitempty"` + D string `json:"data,omitempty"` +} + +func (r *capsule) printerr(err error) string { + buf := strings.Builder{} + r.C = http.StatusInternalServerError + r.D = base64.StdEncoding.EncodeToString(stringToBytes(err.Error())) + _ = json.NewEncoder(&buf).Encode(r) + return buf.String() +} + +func (r *capsule) printstrerr(err string) string { + buf := strings.Builder{} + r.C = http.StatusInternalServerError + r.D = base64.StdEncoding.EncodeToString(stringToBytes(err)) + _ = json.NewEncoder(&buf).Encode(r) + return buf.String() +} + +//export request +func request(para *C.char) *C.char { + r := capsule{} + err := json.Unmarshal(stringToBytes(C.GoString(para)), &r) + if err != nil { + return C.CString(r.printerr(err)) + } + if r.U == "" || !strings.HasPrefix(r.U, "http") { + return C.CString(r.printstrerr("invalid url '" + r.U + "'")) + } + if r.M != "GET" && r.M != "POST" && r.M != "DELETE" { + return C.CString(r.printstrerr("invalid method '" + r.U + "'")) + } + req, err := http.NewRequest(r.M, r.U, strings.NewReader(r.D)) + if err != nil { + return C.CString(r.printerr(err)) + } + for k, vs := range r.H { + lk := strings.ToLower(k) + if strings.HasPrefix(lk, "x-") { + continue + } + switch x := vs.(type) { + case string: + req.Header.Add(k, x) + case []string: + for _, v := range x { + req.Header.Add(k, v) + } + default: + return C.CString(r.printstrerr("unsupported H type " + reflect.ValueOf(x).Type().Name())) + } + } + resp, err := cli.Do(req) + if err != nil { + return C.CString(r.printerr(err)) + } + defer resp.Body.Close() + sb := strings.Builder{} + enc := base64.NewEncoder(base64.StdEncoding, &sb) + _, err = io.CopyN(enc, resp.Body, resp.ContentLength) + _ = enc.Close() + if err != nil { + return C.CString(r.printerr(err)) + } + r.C = resp.StatusCode + r.H = make(map[string]any, len(resp.Header)*2) + for k, vs := range resp.Header { + if len(vs) == 1 { + r.H[k] = vs[0] + continue + } + r.H[k] = vs + } + r.D = sb.String() + outbuf := strings.Builder{} + err = json.NewEncoder(&outbuf).Encode(&r) + if err != nil { + return C.CString(r.printerr(err)) + } + return C.CString(outbuf.String()) +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..edf0da1 --- /dev/null +++ b/utils.go @@ -0,0 +1,32 @@ +package main + +import "unsafe" + +// slice is the runtime representation of a slice. +// It cannot be used safely or portably and its representation may +// change in a later release. +// +// Unlike reflect.SliceHeader, its Data field is sufficient to guarantee the +// data it references will not be garbage collected. +type slice struct { + data unsafe.Pointer + len int + cap int +} + +/* +// bytesToString 没有内存开销的转换 +func bytesToString(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} +*/ + +// stringToBytes 没有内存开销的转换 +func stringToBytes(s string) (b []byte) { + bh := (*slice)(unsafe.Pointer(&b)) + sh := (*slice)(unsafe.Pointer(&s)) + bh.data = sh.data + bh.len = sh.len + bh.cap = sh.len + return b +}