From fd8311b2803ac594dc557617a1b9354e88142f0f Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Thu, 28 Mar 2024 14:11:51 -0400 Subject: [PATCH] Add socket operations --- builtins/accept.go | 32 ++++++++++++++++++ builtins/bind.go | 54 ++++++++++++++++++++++++++++++ builtins/builtins.go | 5 +++ builtins/connect.go | 50 ++++++++++++++++++++++++++++ builtins/listen.go | 27 +++++++++++++++ builtins/socket.go | 65 ++++++++++++++++++++++++++++++++++++ builtins/socket_utils.go | 68 ++++++++++++++++++++++++++++++++++++++ compiler/compiler_test.go | 6 ++-- evaluator/evaluator.go | 4 +-- examples/echoclient.monkey | 8 +++++ examples/echoserver.monkey | 11 ++++++ 11 files changed, 325 insertions(+), 5 deletions(-) create mode 100644 builtins/accept.go create mode 100644 builtins/bind.go create mode 100644 builtins/connect.go create mode 100644 builtins/listen.go create mode 100644 builtins/socket.go create mode 100644 builtins/socket_utils.go create mode 100644 examples/echoclient.monkey create mode 100644 examples/echoserver.monkey diff --git a/builtins/accept.go b/builtins/accept.go new file mode 100644 index 0000000..7b776af --- /dev/null +++ b/builtins/accept.go @@ -0,0 +1,32 @@ +package builtins + +import ( + "monkey/object" + "monkey/typing" + "syscall" +) + +// Accept ... +func Accept(args ...object.Object) object.Object { + if err := typing.Check( + "accept", args, + typing.ExactArgs(1), + typing.WithTypes(object.INTEGER_OBJ), + ); err != nil { + return newError(err.Error()) + } + + var ( + nfd syscall.Handle + err error + ) + + fd := int(args[0].(*object.Integer).Value) + + nfd, _, err = syscall.Accept(syscall.Handle(fd)) + if err != nil { + return newError("SocketError: %s", err) + } + + return &object.Integer{Value: int64(nfd)} +} diff --git a/builtins/bind.go b/builtins/bind.go new file mode 100644 index 0000000..fda7f83 --- /dev/null +++ b/builtins/bind.go @@ -0,0 +1,54 @@ +package builtins + +import ( + "monkey/object" + "monkey/typing" + "syscall" +) + +// Bind ... +func Bind(args ...object.Object) object.Object { + if err := typing.Check( + "bind", args, + typing.ExactArgs(2), + typing.WithTypes(object.INTEGER_OBJ, object.INTEGER_OBJ), + ); err != nil { + return newError(err.Error()) + } + + var ( + err error + sockaddr syscall.Sockaddr + ) + + fd := int(args[0].(*object.Integer).Value) + address := args[1].(*object.String).Value + + sockaddr, err = syscall.Getsockname(syscall.Handle(fd)) + if err != nil { + return newError("ValueError: %s", err) + } + + if _, ok := sockaddr.(*syscall.SockaddrInet4); ok { + addr, port, err := parseV4Address(address) + if err != nil { + return newError("ValueError: Invalid IPv4 address '%s': %s", address, err) + } + sockaddr = &syscall.SockaddrInet4{Addr: addr, Port: port} + } else if _, ok := sockaddr.(*syscall.SockaddrInet6); ok { + addr, port, err := parseV6Address(address) + if err != nil { + return newError("ValueError: Invalid IPv6 address '%s': %s", address, err) + } + sockaddr = &syscall.SockaddrInet6{Addr: addr, Port: port} + } else { + return newError("ValueError: Invalid socket type %T for bind '%s'", sockaddr, address) + } + + err = syscall.Bind(syscall.Handle(fd), sockaddr) + if err != nil { + return newError("SocketError: %s", err) + } + + return &object.Null{} +} diff --git a/builtins/builtins.go b/builtins/builtins.go index bec2689..f3275dc 100644 --- a/builtins/builtins.go +++ b/builtins/builtins.go @@ -50,6 +50,11 @@ var Builtins = map[string]*object.Builtin{ "write": {Name: "write", Fn: Write}, "read": {Name: "read", Fn: Read}, "seek": {Name: "seek", Fn: Seek}, + "socket": {Name: "socket", Fn: Socket}, + "bind": {Name: "bind", Fn: Bind}, + "accept": {Name: "accept", Fn: Accept}, + "listen": {Name: "listen", Fn: Listen}, + "connect": {Name: "connect", Fn: Connect}, } // BuiltinsIndex ... diff --git a/builtins/connect.go b/builtins/connect.go new file mode 100644 index 0000000..eee9dd6 --- /dev/null +++ b/builtins/connect.go @@ -0,0 +1,50 @@ +package builtins + +import ( + "monkey/object" + "monkey/typing" + "syscall" +) + +// Connect ... +func Connect(args ...object.Object) object.Object { + if err := typing.Check( + "connect", args, + typing.ExactArgs(2), + typing.WithTypes(object.INTEGER_OBJ, object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) + } + + var sa syscall.Sockaddr + + fd := int(args[0].(*object.Integer).Value) + address := args[1].(*object.String).Value + + sockaddr, err := syscall.Getsockname(syscall.Handle(fd)) + if err != nil { + return newError("ValueError: %s", err) + } + + if _, ok := sockaddr.(*syscall.SockaddrInet4); ok { + addr, port, err := parseV4Address(address) + if err != nil { + return newError("ValueError: Invalid IPv4 address '%s': %s", address, err) + } + sa = &syscall.SockaddrInet4{Addr: addr, Port: port} + } else if _, ok := sockaddr.(*syscall.SockaddrInet6); ok { + addr, port, err := parseV6Address(address) + if err != nil { + return newError("ValueError: Invalid IPv6 address '%s': %s", address, err) + } + sa = &syscall.SockaddrInet6{Addr: addr, Port: port} + } else { + return newError("ValueError: Invalid socket type %T for bind '%s'", sockaddr, address) + } + + if err = syscall.Connect(syscall.Handle(fd), sa); err != nil { + return newError("SocketError: %s", err) + } + + return &object.Null{} +} diff --git a/builtins/listen.go b/builtins/listen.go new file mode 100644 index 0000000..0103351 --- /dev/null +++ b/builtins/listen.go @@ -0,0 +1,27 @@ +package builtins + +import ( + "monkey/object" + "monkey/typing" + "syscall" +) + +// Listen ... +func Listen(args ...object.Object) object.Object { + if err := typing.Check( + "listen", args, + typing.ExactArgs(2), + typing.WithTypes(object.INTEGER_OBJ, object.INTEGER_OBJ), + ); err != nil { + return newError(err.Error()) + } + + fd := int(args[0].(*object.Integer).Value) + backlog := int(args[1].(*object.Integer).Value) + + if err := syscall.Listen(syscall.Handle(fd), backlog); err != nil { + return newError("SocketError: %s", err) + } + + return &object.Null{} +} diff --git a/builtins/socket.go b/builtins/socket.go new file mode 100644 index 0000000..4e71d16 --- /dev/null +++ b/builtins/socket.go @@ -0,0 +1,65 @@ +package builtins + +import ( + "monkey/object" + "monkey/typing" + "strings" + "syscall" +) + +// Socket ... +func Socket(args ...object.Object) object.Object { + if err := typing.Check( + "socket", args, + typing.ExactArgs(1), + typing.WithTypes(object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) + } + + var ( + domain int + typ int + proto int + ) + + arg := args[0].(*object.String).Value + + switch strings.ToLower(arg) { + case "unix": + domain = syscall.AF_UNIX + typ = syscall.SOCK_STREAM + proto = 0 + case "tcp4": + domain = syscall.AF_INET + typ = syscall.SOCK_STREAM + proto = syscall.IPPROTO_TCP + case "tcp6": + domain = syscall.AF_INET6 + typ = syscall.SOCK_STREAM + proto = syscall.IPPROTO_TCP + case "udp4": + domain = syscall.AF_INET + typ = syscall.SOCK_DGRAM + proto = syscall.IPPROTO_UDP + case "udp6": + domain = syscall.AF_INET6 + typ = syscall.SOCK_DGRAM + proto = syscall.IPPROTO_UDP + default: + return newError("ValueError: invalid socket type '%s'", arg) + } + + fd, err := syscall.Socket(domain, typ, proto) + if err != nil { + return newError("SocketError: %s", err) + } + + if domain == syscall.AF_INET || domain == syscall.AF_INET6 { + if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { + return newError("SocketError: cannot enable SO_REUSEADDR: %s", err) + } + } + + return &object.Integer{Value: int64(fd)} +} diff --git a/builtins/socket_utils.go b/builtins/socket_utils.go new file mode 100644 index 0000000..0f40050 --- /dev/null +++ b/builtins/socket_utils.go @@ -0,0 +1,68 @@ +package builtins + +import ( + "fmt" + "net" + "strconv" +) + +func parseAddress(address string) (ip net.IP, port int, err error) { + var ( + h string + p string + n int64 + ) + + h, p, err = net.SplitHostPort(address) + if err != nil { + return + } + + ip = net.ParseIP(h) + if ip == nil { + var addrs []string + addrs, err = net.LookupHost(address) + if err != nil { + err = fmt.Errorf("error resolving host '%s'", address) + return + } + + if len(addrs) == 0 { + err = fmt.Errorf("host not found '%s'", address) + return + } + + ip = net.ParseIP(addrs[0]) + if ip == nil { + err = fmt.Errorf("invalid IP address '%s'", address) + return + } + } + + n, err = strconv.ParseInt(p, 10, 16) + if err != nil { + return + } + port = int(n) + return +} + +func parseV4Address(address string) (addr [4]byte, port int, err error) { + var ip net.IP + ip, port, err = parseAddress(address) + if err != nil { + return + } + copy(addr[:], ip.To4()[:4]) + return +} + +func parseV6Address(address string) (addr [16]byte, port int, err error) { + var ip net.IP + ip, port, err = parseAddress(address) + if err != nil { + return + } + copy(addr[:], ip.To16()[:16]) + return +} diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index cbdb16e..8100aba 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -899,11 +899,11 @@ func TestBuiltins(t *testing.T) { `, expectedConstants: []interface{}{1}, expectedInstructions: []code.Instructions{ - code.Make(code.OpGetBuiltin, 19), + code.Make(code.OpGetBuiltin, 22), code.Make(code.OpArray, 0), code.Make(code.OpCall, 1), code.Make(code.OpPop), - code.Make(code.OpGetBuiltin, 29), + code.Make(code.OpGetBuiltin, 33), code.Make(code.OpArray, 0), code.Make(code.OpConstant, 0), code.Make(code.OpCall, 2), @@ -914,7 +914,7 @@ func TestBuiltins(t *testing.T) { input: `fn() { return len([]) }`, expectedConstants: []interface{}{ []code.Instructions{ - code.Make(code.OpGetBuiltin, 19), + code.Make(code.OpGetBuiltin, 22), code.Make(code.OpArray, 0), code.Make(code.OpCall, 1), code.Make(code.OpReturn), diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index cde90b4..9a7a565 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -414,9 +414,9 @@ func evalBooleanInfixExpression(operator string, left, right object.Object) obje switch operator { case "&&": - return &object.Boolean{Value: leftVal && rightVal} + return nativeBoolToBooleanObject(leftVal && rightVal) case "||": - return &object.Boolean{Value: leftVal || rightVal} + return nativeBoolToBooleanObject(leftVal || rightVal) default: return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type()) } diff --git a/examples/echoclient.monkey b/examples/echoclient.monkey new file mode 100644 index 0000000..629abe6 --- /dev/null +++ b/examples/echoclient.monkey @@ -0,0 +1,8 @@ +#!./monkey-lang + +fd := socket("tcp4") +bind(fd, "127.0.0.1:32535") +connect(fd, "127.0.0.1:8000") +write(fd, "Hello World") +print(read(fd)) +close(fd) \ No newline at end of file diff --git a/examples/echoserver.monkey b/examples/echoserver.monkey new file mode 100644 index 0000000..76c8567 --- /dev/null +++ b/examples/echoserver.monkey @@ -0,0 +1,11 @@ +#!./monkey-lang + +fd := socket("tcp4") +bind(fd, "0.0.0.0:8000") +listen(fd, 1) + +nfd := accept(fd) +msg := read(nfd) +write(nfd, msg) +close(nfd) +close(fd) \ No newline at end of file