diff --git a/CHANGELOG.md b/CHANGELOG.md index f94e5eec20..235462385d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New gpio driver for STM32 with nif and port support for read and write functions. - Added support for interrupts to STM32 GPIO port driver. - Added suppoprt for PicoW extra gpio pins (led) to the gpio driver. +- Added support for `net:getaddrinfo/1,2` ## [0.6.0-alpha.1] - 2023-10-09 diff --git a/doc/src/programmers-guide.md b/doc/src/programmers-guide.md index 4f075bd05d..217fb21439 100644 --- a/doc/src/programmers-guide.md +++ b/doc/src/programmers-guide.md @@ -1860,3 +1860,49 @@ For example: end Close a UDP socket just as you would a TCP socket, as described above. + +### Miscellaneous Networking APIs + +You can retrieve information about hostnames and services using the `net:getaddrinfo/1` and `net:getaddrinfo/2` functions. The return value is a list of maps each of which contains address information about the host, including its family (`inet`), protocol (`tcp` or `udp`), type (`stream` or `dgram`), and the address, currently an IPv4 tuple. + +> Note. Currently, the `net:getaddrinfo/1,2` functions only supports reporting of IPv4 addresses. + +For example: + + %% erlang + {ok, AddrInfos} = net:getaddrinfo("www.atomvm.net"), + + lists:foreach( + fun(AddrInfo) -> + #{ + family := Family, + protocol := Protocol, + type := Type, + address := Address + } = AddrInfo, + + io:format("family: ~p prototcol: ~p type: ~p address: ~p", [Family, Protocol, Type, Address]) + + end, + AddrInfos + ), + +The `host` parameter can be a domain name (typically) or a dotted pair IPv4 address. + +The returned map contains the network family (currently, only `inet` is supported), the protocol, type, and address of the host. + +The address is itself a map, containing the family, port and IPv4 address of the requested host, e.g., + + #{family => inet, port => 0, addr => {192, 168, 212, 153}} + +> Note. The [OTP documentation](https://www.erlang.org/doc/man/net#type-address_info) states that the address is returned under the `address` key in the address info map. However, OTP appears to use `addr` as the key. For compatibility with OTP 22 ff., AtomVM supports both the `address` and `addr` keys in this map (they reference the same inner map). + +If you want to narrow the information you get back to a specific service type, you can specify a service name or port number (as a string value) as the second parameter: + + %% erlang + {ok, AddrInfos} = net:getaddrinfo("www.atomvm.net", "https"), + ... + +Service names are well-known identifiers on the internet, but they may vary from operating system to operating system. See the `services(3)` man pages for more information. + +> Note. Narrowing results via the service parameter is not supported on all platforms. In the case where it is not supported, AtomVM will resort to retrying the request without the service parameter. diff --git a/libs/estdlib/src/CMakeLists.txt b/libs/estdlib/src/CMakeLists.txt index db0a1f2ba3..35c5db10b9 100644 --- a/libs/estdlib/src/CMakeLists.txt +++ b/libs/estdlib/src/CMakeLists.txt @@ -41,6 +41,7 @@ set(ERLANG_MODULES lists maps math + net logger logger_std_h proplists diff --git a/libs/estdlib/src/net.erl b/libs/estdlib/src/net.erl new file mode 100644 index 0000000000..73385a333e --- /dev/null +++ b/libs/estdlib/src/net.erl @@ -0,0 +1,80 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Fred Dushin +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(net). + +-export([getaddrinfo/1, getaddrinfo/2]). + +%% nif call (so we can use guards at the API) +-export([getaddrinfo_nif/2]). + +-type addrinfo() :: #{ + family := socket:domain(), + socktype := socket:type(), + protocol := socket:protocol(), + %% NB. The erlang docs appear to be wrong about this + address := socket:sockaddr(), + addr := socket:sockaddr() +}. + +-type service() :: string(). + +-export_type([addrinfo/0, service/0]). + +%%----------------------------------------------------------------------------- +%% @param Host the host string for which to find address information +%% @returns Address info for the specified host +%% @equiv getaddrinfo(Host, undefined) +%% @doc Retrieve address information for a given hostname. +%% @end +%%----------------------------------------------------------------------------- +-spec getaddrinfo(Host :: string()) -> {ok, AddrInfo :: addrinfo()} | {error, Reason :: term()}. +getaddrinfo(Host) when is_list(Host) -> + ?MODULE:getaddrinfo(Host, undefined). + +%%----------------------------------------------------------------------------- +%% @param Host the host string for which to find address information +%% @param Service the service string for which to find address information +%% @returns Address info for the specified host and service +%% @doc Retrieve address information for a given hostname and service. +%% +%% The `Host' parameter may be a fully qualified host name or a string +%% containing a valid dotted pair IP address. (Currently, only IPv4 is +%% supported). +%% +%% The `Service' parameter may be the name of a service (as defined via +%% `services(3)` or a string containing a decimal value of the same. +%% +%% Note that the `Host' or `String' parameter may be `undefined', but +%% not both. +%% @end +%%----------------------------------------------------------------------------- +-spec getaddrinfo(Host :: string() | undefined, Service :: service() | undefined) -> + {ok, AddrInfo :: addrinfo()} | {error, Reason :: term()}. +getaddrinfo(Host, Service) when + (is_list(Host) orelse Host =:= undefined) andalso + (is_list(Service) orelse Service =:= undefined) andalso + not (Host =:= undefined andalso Service =:= undefined) +-> + ?MODULE:getaddrinfo_nif(Host, Service). + +%% @hidden +getaddrinfo_nif(_Host, _Service) -> + erlang:nif_error(undefined). diff --git a/src/libAtomVM/interop.c b/src/libAtomVM/interop.c index b6b6479399..f4be29c406 100644 --- a/src/libAtomVM/interop.c +++ b/src/libAtomVM/interop.c @@ -611,3 +611,25 @@ term interop_kv_get_value_default(term kv, AtomString key, term default_value, G return default_value; } } + +term interop_atom_term_select_atom(const AtomStringIntPair *table, int value, GlobalContext *global) +{ + int i; + for (i = 0; table[i].as_val != NULL; i++) { + if (value == table[i].i_val) { + int global_atom_index = globalcontext_insert_atom(global, table[i].as_val); + return term_from_atom_index(global_atom_index); + } + } + return term_invalid_term(); +} + +term interop_chars_to_list(const char *chars, size_t len, Heap *heap) +{ + term ret = term_nil(); + for (int i = (int) len - 1; i >= 0; --i) { + term c_term = term_from_int(chars[i]); + ret = term_list_prepend(c_term, ret, heap); + } + return ret; +} diff --git a/src/libAtomVM/interop.h b/src/libAtomVM/interop.h index d90d849574..edb3091442 100644 --- a/src/libAtomVM/interop.h +++ b/src/libAtomVM/interop.h @@ -74,6 +74,7 @@ term interop_proplist_get_value(term list, term key); term interop_proplist_get_value_default(term list, term key, term default_value); term interop_map_get_value(GlobalContext *glb, term map, term key); term interop_map_get_value_default(GlobalContext *glb, term map, term key, term default_value); +term interop_chars_to_list(const char *chars, size_t len, Heap *heap); NO_DISCARD InteropFunctionResult interop_iolist_size(term t, size_t *size); NO_DISCARD InteropFunctionResult interop_write_iolist(term t, char *p); @@ -95,7 +96,7 @@ NO_DISCARD enum UnicodeConversionResult interop_chardata_to_bytes(term t, uint8_ * @details Allows to quickly translate atoms to any integer constant. This function is useful for * creating switch statements for atom values. * A linear search is performed, so table entries should be sorted by frequency. - * @param table an array AtomStringIntPair structs, teminated with a default entry marked with + * @param table an array AtomStringIntPair structs, terminated with a default entry marked with * SELECT_INT_DEFAULT macro. * @param atom the atom used for comparison. * @param global the global context. @@ -103,6 +104,19 @@ NO_DISCARD enum UnicodeConversionResult interop_chardata_to_bytes(term t, uint8_ */ int interop_atom_term_select_int(const AtomStringIntPair *table, term atom, GlobalContext *global); +/** + * @brief Finds the first matching atom in an atoms table . + * + * @details Allows to quickly translate integer constants to an atom in an atoms table. + * This function is the inverse of interop_atom_term_select_int. + * @param table an array AtomStringIntPair structs, terminated with a default entry marked with + * SELECT_INT_DEFAULT macro. + * @param value the in value used for comparison. + * @param global the global context. + * @returns the found atom which corresponds to the given int value. + */ +term interop_atom_term_select_atom(const AtomStringIntPair *table, int value, GlobalContext *global); + /** * @brief Get a value given a key (as AtomString) from any proplist or map * diff --git a/src/libAtomVM/otp_net.c b/src/libAtomVM/otp_net.c new file mode 100644 index 0000000000..2be903822f --- /dev/null +++ b/src/libAtomVM/otp_net.c @@ -0,0 +1,310 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 by Fred Dushin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// #define ENABLE_TRACE +#include + +#define UNKNOWN_TABLE_VALUE -1 + +static const AtomStringIntPair protocol_table[] = { + { ATOM_STR("\x3", "tcp"), IPPROTO_TCP }, + { ATOM_STR("\x3", "udp"), IPPROTO_UDP }, + SELECT_INT_DEFAULT(UNKNOWN_TABLE_VALUE) +}; + +static const AtomStringIntPair type_table[] = { + { ATOM_STR("\x5", "dgram"), SOCK_DGRAM }, + { ATOM_STR("\x6", "stream"), SOCK_STREAM }, + SELECT_INT_DEFAULT(UNKNOWN_TABLE_VALUE) +}; + +// +// utilities +// + +static term socket_tuple_from_addr(Context *ctx, uint32_t addr) +{ + term terms[4]; + terms[0] = term_from_int32((addr >> 24) & 0xFF); + terms[1] = term_from_int32((addr >> 16) & 0xFF); + terms[2] = term_from_int32((addr >> 8) & 0xFF); + terms[3] = term_from_int32(addr & 0xFF); + + return port_create_tuple_n(ctx, 4, terms); +} + +static inline term make_error_tuple(term reason, Context *ctx) +{ + term error_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(error_tuple, 0, ERROR_ATOM); + term_put_tuple_element(error_tuple, 1, reason); + return error_tuple; +} + +static term eai_errno_to_term(int err, GlobalContext *glb) +{ + switch (err) { + case EAI_AGAIN: + return globalcontext_make_atom(glb, ATOM_STR("\x8", "eaiagain")); + case EAI_BADFLAGS: + return globalcontext_make_atom(glb, ATOM_STR("\xB", "eaibadflags")); + case EAI_FAIL: + return globalcontext_make_atom(glb, ATOM_STR("\x7", "eaifail")); + case EAI_FAMILY: + return globalcontext_make_atom(glb, ATOM_STR("\x9", "eaifamily")); + case EAI_MEMORY: + return globalcontext_make_atom(glb, ATOM_STR("\x9", "eaimemory")); + case EAI_NONAME: + return globalcontext_make_atom(glb, ATOM_STR("\x9", "eainoname")); + case EAI_SERVICE: + return globalcontext_make_atom(glb, ATOM_STR("\xA", "eaiservice")); + case EAI_SOCKTYPE: + return globalcontext_make_atom(glb, ATOM_STR("\xB", "eaisocktype")); +#ifdef HAVE_EXTENDED_EAI_ERRNO + case EAI_BADHINTS: + return globalcontext_make_atom(glb, ATOM_STR("\xB", "eaibadhints")); + case EAI_OVERFLOW: + return globalcontext_make_atom(glb, ATOM_STR("\xB", "eaioverflow")); + case EAI_PROTOCOL: + return globalcontext_make_atom(glb, ATOM_STR("\xB", "eaiprotocol")); + case EAI_SYSTEM: + return globalcontext_make_atom(glb, ATOM_STR("\x9", "eaisystem")); +#endif + } + return term_from_int(err); +} + +static inline term make_address(Context *ctx, struct sockaddr_in *addr) +{ + return socket_tuple_from_addr(ctx, ntohl(addr->sin_addr.s_addr)); +} + +// +// net:getaddrinfo/1 +// + +static term nif_net_getaddrinfo(Context *ctx, int argc, term argv[]) +{ + TRACE("nif_net_getaddrinfo\n"); + UNUSED(argc); + + GlobalContext *global = ctx->global; + + term host = argv[0]; + term service = argv[1]; + + if (host == UNDEFINED_ATOM && service == UNDEFINED_ATOM) { + TRACE("Host and Service params may not both be undefined"); + RAISE_ERROR(BADARG_ATOM); + } + + char *host_str = NULL; + if (host != UNDEFINED_ATOM) { + int ok; + host_str = interop_term_to_string(host, &ok); + if (!ok) { + RAISE_ERROR(BADARG_ATOM); + } + TRACE("Host: %s\n", host_str); + } + + char *service_str = NULL; + if (service != UNDEFINED_ATOM) { + int ok; + service_str = interop_term_to_string(service, &ok); + if (!ok) { + free(host_str); + RAISE_ERROR(BADARG_ATOM); + } + TRACE("Service: %s\n", service_str); + } + + avm_uint_t port = 0; +#ifdef HAVE_SERVBYNAME + if (!IS_NULL_PTR(service_str)) { + struct servent *svt = getservbyname(service_str, NULL); + if (!IS_NULL_PTR(svt)) { + port = ntohs(svt->s_port); + } + } +#endif + TRACE("port: %zu\n", port); + + // for now, we are only supporting IPv4 addresses + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_INET; + + struct addrinfo *host_info; + int err = getaddrinfo(host_str, service_str, &hints, &host_info); + + // some implementations do not support service filters + if (err == EAI_SERVICE) { + fprintf(stderr, "WARNING: EAI_SERVICE unsupported on this platform.\n"); + err = getaddrinfo(host_str, NULL, &hints, &host_info); + } + + free(host_str); + free(service_str); + + if (err != 0 && err != EAI_SERVICE) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return make_error_tuple(eai_errno_to_term(err, global), ctx); + } + TRACE("getaddrinfo succeeded\n"); + + size_t num_addrinfos = 0; + for (struct addrinfo *p = host_info; p != NULL; p = p->ai_next) { + num_addrinfos++; + } + TRACE("num_addrinfos: %zu\n", num_addrinfos); + + if (num_addrinfos == 0) { + return term_nil(); + } + + // {ok, [#{ + // family => Family :: atom() + // protocol => Protocol :: atom() + // type -> Type :: atom() + // address, addr => + // #{ + // addr => Address :: {0..255, 0..255, 0..255, 0..255}, + // port => 0..65535, + // family => inet + // } + // }]} + // Note. We might over-allocate for some more esoteric calls + size_t requested_size = TUPLE_SIZE(2) + LIST_SIZE(num_addrinfos, (term_map_size_in_terms(4) + term_map_size_in_terms(3) + TUPLE_SIZE(4))); + if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term infos = term_nil(); + for (struct addrinfo *p = host_info; p != NULL; p = p->ai_next) { + + // the number of elements in the outer map. Add 1 because address and addr will point to the same (inner) map + // for compatibility with OTP behavior + // Cf. https://erlangforums.com/t/discrepancy-in-type-spec-for-net-getaddrinfo/2984 + int num_elts = (p->ai_family != 0) + (p->ai_protocol != 0) + (p->ai_socktype != 0) + (p->ai_addrlen != 0) + 1; + term elt = term_alloc_map(num_elts, &ctx->heap); + + // in the current implementation, this will always be `inet` + term family_atom = globalcontext_make_atom(global, ATOM_STR("\x6", "family")); + term family = globalcontext_make_atom(global, ATOM_STR("\x4", "inet")); + int i = 0; + term_set_map_assoc(elt, i++, family_atom, family); + + if (p->ai_protocol) { + term protocol_atom = globalcontext_make_atom(global, ATOM_STR("\x8", "protocol")); + term protocol = interop_atom_term_select_atom(protocol_table, p->ai_protocol, global); + term_set_map_assoc(elt, i++, protocol_atom, term_is_invalid_term(protocol) ? UNDEFINED_ATOM : protocol); + } else { + TRACE("No protocol defined\n"); + } + + if (p->ai_socktype) { + term type_atom = globalcontext_make_atom(global, ATOM_STR("\x4", "type")); + term type = interop_atom_term_select_atom(type_table, p->ai_socktype, global); + term_set_map_assoc(elt, i++, type_atom, term_is_invalid_term(type) ? UNDEFINED_ATOM : type); + } else { + TRACE("No socket type defined\n"); + } + + if (p->ai_addrlen == sizeof(struct sockaddr_in)) { + // The inner addr contains a family, port, and addr + term addr_atom = globalcontext_make_atom(global, ATOM_STR("\x4", "addr")); + term inner_addr = term_alloc_map(3, &ctx->heap); + term_set_map_assoc(inner_addr, 0, family_atom, family); + term_set_map_assoc(inner_addr, 1, PORT_ATOM, term_from_int(port)); + term address = make_address(ctx, (struct sockaddr_in *) p->ai_addr); + term_set_map_assoc(inner_addr, 2, addr_atom, address); + + // embed the inner_addr, but reference it from both address and addr + // for compatibility with OTP + term address_atom = globalcontext_make_atom(global, ATOM_STR("\x7", "address")); + term_set_map_assoc(elt, i++, address_atom, inner_addr); + term_set_map_assoc(elt, i++, addr_atom, inner_addr); + } else { + TRACE("No address defined\n"); + } + infos = term_list_prepend(elt, infos, &ctx->heap); + } + freeaddrinfo(host_info); + + term ret = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(ret, 0, OK_ATOM); + term_put_tuple_element(ret, 1, infos); + +#ifdef ENABLE_TRACE + fprintf(stdout, "host info: "); + term_display(stdout, ret, ctx); + fprintf(stdout, "\n"); +#endif + + return ret; +} + +// +// Nifs +// + +static const struct Nif net_getaddrinfo_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_net_getaddrinfo +}; + +// +// Entrypoints +// + +const struct Nif *otp_net_nif_get_nif(const char *nifname) +{ + if (strncmp("net:", nifname, 4) == 0) { + const char *rest = nifname + 4; + if (strcmp("getaddrinfo_nif/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &net_getaddrinfo_nif; + } + } + return NULL; +} + +void otp_net_init(GlobalContext *global) +{ + UNUSED(global); + + // noop +} diff --git a/src/libAtomVM/otp_net.h b/src/libAtomVM/otp_net.h new file mode 100644 index 0000000000..916ad89169 --- /dev/null +++ b/src/libAtomVM/otp_net.h @@ -0,0 +1,38 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 Fred Dushin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef _OTP_NET_H_ +#define _OTP_NET_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +const struct Nif *otp_net_nif_get_nif(const char *nifname); +void otp_net_init(GlobalContext *global); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index af0613abd3..d605180a0c 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -87,6 +87,7 @@ extern "C" { #define TUPLE_SIZE(elems) ((int) (elems + 1)) #define CONS_SIZE 2 #define REFC_BINARY_CONS_OFFSET 4 +#define LIST_SIZE(num_elements, element_size) ((num_elements * element_size) * CONS_SIZE) #define TERM_BINARY_SIZE_IS_HEAP(size) ((size) < REFC_BINARY_MIN) diff --git a/src/platforms/esp32/components/avm_builtins/CMakeLists.txt b/src/platforms/esp32/components/avm_builtins/CMakeLists.txt index 3762a92a51..d5063dc3ae 100644 --- a/src/platforms/esp32/components/avm_builtins/CMakeLists.txt +++ b/src/platforms/esp32/components/avm_builtins/CMakeLists.txt @@ -28,6 +28,7 @@ set(AVM_BUILTIN_COMPONENT_SRCS "socket_driver.c" "spi_driver.c" "uart_driver.c" + "otp_net_platform.c" "otp_socket_platform.c" ) diff --git a/src/platforms/esp32/components/avm_builtins/Kconfig b/src/platforms/esp32/components/avm_builtins/Kconfig index 2f8fec6c55..94c4c7f009 100644 --- a/src/platforms/esp32/components/avm_builtins/Kconfig +++ b/src/platforms/esp32/components/avm_builtins/Kconfig @@ -70,4 +70,8 @@ config AVM_ENABLE_OTP_SOCKET_NIFS bool "Enable OTP Socket NIFs" default y +config AVM_ENABLE_OTP_NET_NIFS + bool "Enable OTP Net NIFs" + default y + endmenu diff --git a/src/platforms/esp32/components/avm_builtins/otp_net_platform.c b/src/platforms/esp32/components/avm_builtins/otp_net_platform.c new file mode 100644 index 0000000000..c791640611 --- /dev/null +++ b/src/platforms/esp32/components/avm_builtins/otp_net_platform.c @@ -0,0 +1,31 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 by Fred Dushin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include +#include +#include +#include + + +#ifdef CONFIG_AVM_ENABLE_OTP_NET_NIFS + +REGISTER_NIF_COLLECTION(otp_net, otp_net_init, NULL, otp_net_nif_get_nif) + +#endif diff --git a/src/platforms/esp32/components/avm_sys/CMakeLists.txt b/src/platforms/esp32/components/avm_sys/CMakeLists.txt index bf4596aa0a..0167a364f3 100644 --- a/src/platforms/esp32/components/avm_sys/CMakeLists.txt +++ b/src/platforms/esp32/components/avm_sys/CMakeLists.txt @@ -25,6 +25,7 @@ set(AVM_SYS_COMPONENT_SRCS "sys.c" "platform_nifs.c" "platform_defaultatoms.c" + "../../../../libAtomVM/otp_net.c" "../../../../libAtomVM/otp_socket.c" ) diff --git a/src/platforms/generic_unix/lib/CMakeLists.txt b/src/platforms/generic_unix/lib/CMakeLists.txt index 807aee3265..d5a2f469fd 100644 --- a/src/platforms/generic_unix/lib/CMakeLists.txt +++ b/src/platforms/generic_unix/lib/CMakeLists.txt @@ -26,6 +26,7 @@ set(HEADER_FILES generic_unix_sys.h mapped_file.h platform_defaultatoms.h + ../../../libAtomVM/otp_net.h ../../../libAtomVM/otp_socket.h ) @@ -37,6 +38,7 @@ set(SOURCE_FILES smp.c socket_driver.c sys.c + ../../../libAtomVM/otp_net.c ../../../libAtomVM/otp_socket.c ) @@ -53,6 +55,7 @@ endif() include(DefineIfExists) define_if_function_exists(libAtomVM${PLATFORM_LIB_SUFFIX} signal "signal.h" PRIVATE HAVE_SIGNAL) +define_if_function_exists(libAtomVM${PLATFORM_LIB_SUFFIX} getservbyname "netdb.h" PRIVATE HAVE_SERVBYNAME) target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC libAtomVM) include_directories(${CMAKE_SOURCE_DIR}/src/platforms/generic_unix/lib) @@ -87,6 +90,11 @@ check_symbol_exists(NOTE_TRIGGER "sys/event.h" HAVE_NOTE_TRIGGER) if (HAVE_KQUEUE AND HAVE_EVFILT_USER AND HAVE_NOTE_TRIGGER) target_compile_definitions(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC HAVE_KQUEUE) endif() +# pick one as an example +check_symbol_exists(EAI_BADHINTS "netdb.h" HAVE_EXTENDED_EAI_ERRNO) +if (HAVE_EXTENDED_EAI_ERRNO) + target_compile_definitions(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC HAVE_EXTENDED_EAI_ERRNO) +endif() if (COVERAGE) include(CodeCoverage) diff --git a/src/platforms/generic_unix/lib/platform_nifs.c b/src/platforms/generic_unix/lib/platform_nifs.c index b666d3d438..3f23072542 100644 --- a/src/platforms/generic_unix/lib/platform_nifs.c +++ b/src/platforms/generic_unix/lib/platform_nifs.c @@ -25,6 +25,7 @@ #include "interop.h" #include "memory.h" #include "nifs.h" +#include "otp_net.h" #include "otp_socket.h" #include "platform_defaultatoms.h" #include "term.h" @@ -261,5 +262,10 @@ const struct Nif *platform_nifs_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &atomvm_platform_nif; } - return otp_socket_nif_get_nif(nifname); + const struct Nif *nif = otp_net_nif_get_nif(nifname); + if (nif == NULL) { + return otp_socket_nif_get_nif(nifname); + } else { + return nif; + } } diff --git a/src/platforms/generic_unix/lib/sys.c b/src/platforms/generic_unix/lib/sys.c index 48f8618ff7..c662e15eb3 100644 --- a/src/platforms/generic_unix/lib/sys.c +++ b/src/platforms/generic_unix/lib/sys.c @@ -25,6 +25,7 @@ #include "defaultatoms.h" #include "iff.h" #include "mapped_file.h" +#include "otp_net.h" #include "otp_socket.h" #include "scheduler.h" #include "smp.h" @@ -559,6 +560,9 @@ void sys_init_platform(GlobalContext *global) #endif #endif + // TODO implement these functions in shlibs and load dynamically + // once we have a framework for that + otp_net_init(global); otp_socket_init(global); global->platform_data = platform; diff --git a/tests/libs/estdlib/CMakeLists.txt b/tests/libs/estdlib/CMakeLists.txt index eb390661d4..b543f4ba59 100644 --- a/tests/libs/estdlib/CMakeLists.txt +++ b/tests/libs/estdlib/CMakeLists.txt @@ -33,6 +33,7 @@ set(ERLANG_MODULES test_lists test_logger test_maps + test_net test_spawn test_string test_proplists diff --git a/tests/libs/estdlib/test_net.erl b/tests/libs/estdlib/test_net.erl new file mode 100644 index 0000000000..37042df43f --- /dev/null +++ b/tests/libs/estdlib/test_net.erl @@ -0,0 +1,143 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Fred Dushin +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_net). + +-export([test/0]). + +test() -> + ok = test_getaddrinfo(), + ok = test_getaddrinfo2(), + ok. + +test_getaddrinfo() -> + ok = test_getaddrinfo("www.atomvm.net"), + ok. + +test_getaddrinfo(Host) -> + {ok, AddrInfos} = net:getaddrinfo(Host), + + lists:foreach( + fun(AddrInfo) -> + inet = maps:get(family, AddrInfo), + Protocol = maps:get(protocol, AddrInfo, undefined), + Type = maps:get(type, AddrInfo, undefined), + Addr = get_addr(AddrInfo), + + case erlang:system_info(machine) of + "BEAM" -> + ok; + _ -> + true = (Protocol =:= tcp orelse Protocol =:= udp orelse Protocol =:= undefined), + true = (Type =:= dgram orelse Type =:= stream orelse Type =:= undefined) + end, + + {A, B, C, D} = Addr, + true = is_integer(A) andalso 0 =< A andalso A =< 255, + true = is_integer(B) andalso 0 =< B andalso B =< 255, + true = is_integer(C) andalso 0 =< C andalso C =< 255, + true = is_integer(D) andalso 0 =< D andalso D =< 255, + + ok + end, + maybe_filter_addrinfos(AddrInfos) + ), + + expect_failure(fun() -> net:getaddrinfo(not_a_list) end, function_clause), + expect_failure(fun() -> net:getaddrinfo([isnot, a, string]) end, badarg), + expect_failure(fun() -> net:getaddrinfo([$i, $m, $p, $r, $o, $p, $e | $r]) end, badarg), + + {error, _Reason} = net:getaddrinfo("ewkrkerkwe.oksjds.wee.org"), + + ok. + +test_getaddrinfo2() -> + ok = test_getaddrinfo2("www.atomvm.net", "https"), + ok = test_getaddrinfo2("www.atomvm.net", "443"), + ok = test_getaddrinfo2(undefined, "443"), + ok. + +test_getaddrinfo2(Host, Service) -> + {ok, AddrInfos} = net:getaddrinfo(Host, Service), + + lists:foreach( + fun(AddrInfo) -> + inet = maps:get(family, AddrInfo), + Protocol = maps:get(protocol, AddrInfo, undefined), + Type = maps:get(type, AddrInfo, undefined), + Addr = get_addr(AddrInfo), + + case erlang:system_info(machine) of + "BEAM" -> + ok; + _ -> + true = (Protocol =:= tcp orelse Protocol =:= udp orelse Protocol =:= undefined), + true = (Type =:= dgram orelse Type =:= stream orelse Type =:= undefined) + end, + + {A, B, C, D} = Addr, + true = is_integer(A) andalso 0 =< A andalso A =< 255, + true = is_integer(B) andalso 0 =< B andalso B =< 255, + true = is_integer(C) andalso 0 =< C andalso C =< 255, + true = is_integer(D) andalso 0 =< D andalso D =< 255, + + ok + end, + maybe_filter_addrinfos(AddrInfos) + ), + + expect_failure(fun() -> net:getaddrinfo(Host, not_a_list) end, function_clause), + expect_failure(fun() -> net:getaddrinfo(Host, [isnot, a, string]) end, badarg), + expect_failure(fun() -> net:getaddrinfo(Host, [$i, $m, $p, $r, $o, $p, $e | $r]) end, badarg), + expect_failure(fun() -> net:getaddrinfo(undefined, undefined) end, function_clause), + + {error, _Reason} = net:getaddrinfo("ewkrkerkwe.oksjds.wee.org", Service), + + ok. + +expect_failure(F, Expected) -> + try + F(), + fail + catch + _:E when E =:= Expected -> + ok + end. + +maybe_filter_addrinfos(AddrInfos) -> + case erlang:system_info(machine) of + "BEAM" -> + lists:filter( + fun(AddrInfo) -> + inet =:= maps:get(family, AddrInfo) + end, + AddrInfos + ); + _ -> + AddrInfos + end. + +get_addr(AddrInfo) -> + case erlang:system_info(machine) of + "BEAM" -> + maps:get(addr, maps:get(addr, AddrInfo)); + _ -> + maps:get(addr, maps:get(address, AddrInfo)) + end. diff --git a/tests/libs/estdlib/tests.erl b/tests/libs/estdlib/tests.erl index b42f75515e..801fed6b68 100644 --- a/tests/libs/estdlib/tests.erl +++ b/tests/libs/estdlib/tests.erl @@ -36,7 +36,7 @@ get_otp_version() -> get_tests(OTPVersion) when (is_integer(OTPVersion) andalso OTPVersion >= 24) orelse OTPVersion == atomvm -> - [test_tcp_socket, test_udp_socket | get_tests(undefined)]; + [test_tcp_socket, test_udp_socket, test_net | get_tests(undefined)]; get_tests(_OTPVersion) -> [ test_lists,