diff --git a/CMakeLists.txt b/CMakeLists.txt index 8dae55e5..4dc4e631 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,8 +3,8 @@ cmake_minimum_required(VERSION 3.10) project (luv C ASM) set(LUV_VERSION_MAJOR 1) -set(LUV_VERSION_MINOR 49) -set(LUV_VERSION_PATCH 2) +set(LUV_VERSION_MINOR 50) +set(LUV_VERSION_PATCH 0) set(LUV_VERSION ${LUV_VERSION_MAJOR}.${LUV_VERSION_MINOR}.${LUV_VERSION_PATCH}) if(NOT ${CMAKE_VERSION} VERSION_LESS "3.5.0") diff --git a/deps/libuv b/deps/libuv index e1095c7a..8fb9cb91 160000 --- a/deps/libuv +++ b/deps/libuv @@ -1 +1 @@ -Subproject commit e1095c7a4373ce00cd8874d8e820de5afb25776e +Subproject commit 8fb9cb919489a48880680a56efecff6a7dfb4504 diff --git a/docs.md b/docs.md index ef74a499..084fa513 100644 --- a/docs.md +++ b/docs.md @@ -2464,6 +2464,54 @@ completed immediately. **Returns:** `integer` or `fail` +### `uv.udp_try_send2(udp, messages, flags)` + +> method form `udp:try_send2(messages, flags)` + +**Parameters:** +- `udp`: `uv_udp_t userdata` +- `messages`: `table` + - `[1, 2, 3, ..., n]` : `table` + - `data` : `buffer` + - `addr` : `table` + - `ip` : `string` + - `port` : `integer` +- `flags`: `nil` (see below) +- `port`: `integer` + +Like `uv.udp_try_send()`, but can send multiple datagrams. +Lightweight abstraction around `sendmmsg(2)`, with a `sendmsg(2)` fallback loop +for platforms that do not support the former. + +`messages` should be an array-like table, where `addr` must be specified +if the `udp` has not already been connected via `udp_connect` (otherwise, it +must be `nil`). + +`flags` is reserved for future extension and must currently be `nil` or `0` or +`{}`. + +Returns the number of messages sent successfully. An error will only be returned +if the first datagram failed to be sent. + +**Returns:** `integer` or `fail` + +```lua +-- If client:connect() was not called +local addr = { ip = "127.0.0.1", port = 1234 } +client:try_send2({ + { data = "Message 1", addr = addr }, + { data = "Message 2", addr = addr }, + { data = "Message 3", addr = addr }, +}) + +-- If client:connect() was called +client:try_send2({ + { data = "Message 1" }, + { data = "Message 2" }, + { data = "Message 3" }, +}) +``` + ### `uv.udp_recv_start(udp, callback)` > method form `udp:recv_start(callback)` diff --git a/src/luv.c b/src/luv.c index cb9c1c7c..f855b74b 100644 --- a/src/luv.c +++ b/src/luv.c @@ -227,6 +227,9 @@ static const luaL_Reg luv_functions[] = { {"udp_set_ttl", luv_udp_set_ttl}, {"udp_send", luv_udp_send}, {"udp_try_send", luv_udp_try_send}, +#if LUV_UV_VERSION_GEQ(1, 50, 0) + {"udp_try_send2", luv_udp_try_send2}, +#endif {"udp_recv_start", luv_udp_recv_start}, {"udp_recv_stop", luv_udp_recv_stop}, #if LUV_UV_VERSION_GEQ(1, 27, 0) @@ -585,6 +588,9 @@ static const luaL_Reg luv_udp_methods[] = { {"set_ttl", luv_udp_set_ttl}, {"send", luv_udp_send}, {"try_send", luv_udp_try_send}, +#if LUV_UV_VERSION_GEQ(1, 50, 0) + {"try_send2", luv_udp_try_send2}, +#endif {"recv_start", luv_udp_recv_start}, {"recv_stop", luv_udp_recv_stop}, #if LUV_UV_VERSION_GEQ(1, 27, 0) diff --git a/src/udp.c b/src/udp.c index d13372ff..bff9bfc4 100644 --- a/src/udp.c +++ b/src/udp.c @@ -324,6 +324,74 @@ static int luv_udp_try_send(lua_State* L) { return 1; } +#if LUV_UV_VERSION_GEQ(1, 50, 0) +static int luv_udp_try_send2(lua_State* L) { + uv_udp_t* handle = luv_check_udp(L, 1); + int err_or_num_datagrams_sent; + unsigned int num_msgs; + // to-be-allocated with the length of num_msgs + struct sockaddr_storage* addrs; + struct sockaddr** addr_ptrs; + unsigned int* counts; + uv_buf_t** bufs; + unsigned int flags = 0; + + luaL_checktype(L, 2, LUA_TTABLE); + num_msgs = lua_rawlen(L, 2); + + // flags param can be nil, an integer, or a table + if (lua_type(L, 3) == LUA_TNUMBER || lua_isnoneornil(L, 3)) { + flags = (unsigned int)luaL_optinteger(L, 3, 0); + } + else if (lua_type(L, 3) == LUA_TTABLE) { + // this is for forwards-compatibility: if flags ever get added, + // we want to be able to take a table + } + else { + return luaL_argerror(L, 3, "expected nil, integer, or table"); + } + + addrs = malloc(sizeof(struct sockaddr_storage) * num_msgs); + addr_ptrs = malloc(sizeof(struct sockaddr_storage*) * num_msgs); + counts = malloc(sizeof(unsigned int) * num_msgs); + bufs = malloc(sizeof(uv_buf_t*) * num_msgs); + for (unsigned int i=0; i UINT_MAX) + return luaL_error(L, "data at index %d contains too many bufs (max is %d)", UINT_MAX); + counts[i] = count; + lua_pop(L, 1); + lua_getfield(L, element_index, "addr"); + int addr_index = lua_gettop(L); + if (!lua_isnoneornil(L, addr_index)) { + lua_getfield(L, addr_index, "ip"); + lua_getfield(L, addr_index, "port"); + addr_ptrs[i] = luv_check_addr(L, &addrs[i], -2, -1); + lua_pop(L, 4); // ip, port, addr, and current array element + } + else { + addr_ptrs[i] = NULL; + lua_pop(L, 2); // addr and current array element + } + } + err_or_num_datagrams_sent = uv_udp_try_send2(handle, num_msgs, bufs, counts, addr_ptrs, flags); + free(addrs); + free(addr_ptrs); + free(counts); + for (unsigned int i=0; idata; lua_State* L = data->ctx->L; diff --git a/tests/test-udp.lua b/tests/test-udp.lua index 6a564b38..152b534c 100644 --- a/tests/test-udp.lua +++ b/tests/test-udp.lua @@ -344,4 +344,78 @@ return require('lib/tap')(function (test) assert(sender:try_send("PING", "127.0.0.1", TEST_PORT)) end end, "1.39.0") + + local function udp_try_send2_test(should_connect) + return function(print, p, expect, uv) + -- If udp_connect is called on the sender, then addr cannot be specified in any messages. + -- Otherwise, it *must* be specified in every message. + local send_addr = { ip = "127.0.0.1", port = TEST_PORT } + local msgs_to_send = { + { data = "PING", addr = send_addr }, + { data = "PING", addr = send_addr }, + { data = "PING", addr = send_addr }, + -- Add in a message that goes to a different address to ensure that + -- messages having a different address are allowed (when not connected) + { data = "PING", addr = { ip = "127.0.0.1", port = TEST_PORT + 1 } }, + { data = { "P", "I", "N", "G" }, addr = send_addr }, + } + local expected_msgs = #msgs_to_send + if not should_connect then + expected_msgs = expected_msgs - 1 + end + -- Remove addr from the messages if we are calling udp_connect + if should_connect then + for _,v in ipairs(msgs_to_send) do + v.addr = nil + end + end + + local recver = uv.new_udp() + assert(recver:bind("0.0.0.0", TEST_PORT)) + + local sender = uv.new_udp() + if should_connect then + assert(uv.udp_connect(sender, send_addr.ip, send_addr.port)) + end + + local msgs_recved = 0 + local recv_cb = function(err, data, addr, flags) + assert(not err, err) + p(data, addr) + + -- empty callback can happen, just return early + if data == nil and addr == nil then + return + end + + assert(addr) + assert(data == "PING") + + msgs_recved = msgs_recved + 1 + if msgs_recved == expected_msgs then + sender:close() + recver:close() + end + end + + assert(recver:recv_start(recv_cb)) + while #msgs_to_send > 0 do + local num_sent = assert(sender:try_send2(msgs_to_send)) + print('sent '..num_sent..' out of '..#msgs_to_send..', expecting '..expected_msgs..' to be received') + for _=1,num_sent do + table.remove(msgs_to_send, 1) + end + end + end + end + + test("udp try_send2 not connected", function(print, p, expect, uv) + local testfn = udp_try_send2_test(false) + return testfn(print, p, expect, uv) + end, "1.50.0") + + test("udp try_send2 connected", function(print, p, expect, uv) + local testfn = udp_try_send2_test(true) + return testfn(print, p, expect, uv) + end, "1.50.0") end)