|
1 | 1 | <div align="center">
|
2 | 2 | <img src="https://pulsenet.dev/images/pulse-networking-social.png" alt="Pulse Networking" width="1200">
|
3 |
| - <h1>pulseudp</h1> |
4 |
| - <p><strong>Raw non-blocking UDP sockets with Go-style ergonomics.</strong></p> |
| 3 | + <h1>pulse::net::udp</h1> |
| 4 | + <p><strong>Raw non-blocking UDP sockets with Go-style ergonomics, in modern C++23.</strong></p> |
5 | 5 | <p>
|
6 | 6 | <a href="#features">Features</a> •
|
7 | 7 | <a href="#why">Why?</a> •
|
8 | 8 | <a href="#usage">Usage</a> •
|
| 9 | + <a href="#build-requirements">Build Requirements</a> • |
| 10 | + <a href="#fetchcontent">FetchContent</a> • |
9 | 11 | <a href="#platform-support">Platform Support</a> •
|
10 | 12 | <a href="#license">License</a>
|
11 | 13 | </p>
|
12 | 14 | </div>
|
13 | 15 |
|
14 | 16 | ---
|
15 | 17 |
|
16 |
| -pulseudp is a cross-platform UDP socket wrapper for C++ that gives you a clean, Go-style interface — no blocking nonsense, no bloated event loops, and no libuv circus. |
| 18 | +`pulse::net::udp` is a minimal, modern, cross-platform UDP socket layer written in pure C++23 — with sane error handling (`std::expected`), zero dependencies, and no framework bloat. |
17 | 19 |
|
18 |
| -Use it as the foundation for anything low-latency and real-time: games, protocols, transport layers, etc. |
| 20 | +It does one thing well: **non-blocking UDP** across Unix and Windows. |
19 | 21 |
|
20 |
| -It’s designed to get out of your way and let the OS do its job. |
| 22 | +## 🚀 Features |
21 | 23 |
|
22 |
| -## Features |
23 |
| - - ✅ Non-blocking UDP I/O (just like Go) |
24 |
| - - ✅ Listen() for servers |
25 |
| - - ✅ Dial() for clients |
26 |
| - - ✅ sendTo() and recvFrom() like you’d expect |
27 |
| - - ✅ Optional send() and recv() with connected semantics |
28 |
| - - ✅ IPv4 + IPv6 support |
29 |
| - - ✅ No third-party dependencies |
30 |
| - - ✅ Portable between Unix and Windows |
31 |
| - - ✅ Easy to test, drop-in for higher-level protocols |
| 24 | +- ✅ Modern C++23 (`std::expected`, no exceptions) |
| 25 | +- ✅ Non-blocking UDP sockets |
| 26 | +- ✅ `Listen()` and `Dial()` like Go |
| 27 | +- ✅ `send()` / `sendTo()` and `recvFrom()` with structured error handling |
| 28 | +- ✅ Zero dependencies |
| 29 | +- ✅ Cross-platform: Unix (Linux/macOS) and Windows (Winsock2) |
| 30 | +- ✅ Dead simple integration |
32 | 31 |
|
33 |
| -## Why? |
| 32 | +## 🧠 Why? |
34 | 33 |
|
35 |
| -Because C++ UDP sockets are a mess: |
36 |
| - - On Unix, you’re writing raw socket()/bind()/recvfrom() boilerplate. |
37 |
| - - On Windows, you’re dancing with Winsock and WSAStartup() just to send a packet. |
38 |
| - - Nobody agrees on non-blocking behavior or even a clean API. |
| 34 | +Because writing portable UDP in C++ is still a flaming trash heap: |
| 35 | +- POSIX and Winsock APIs barely resemble each other |
| 36 | +- Most libraries are bloated, legacy-bound, or layered abstractions on top of boost or libuv |
| 37 | +- Nobody should still be writing socket() / bind() / recvfrom() directly in 2025 |
39 | 38 |
|
40 |
| -Go got it right. So we copied it. |
| 39 | +This library fixes that with a clean, modern API that doesn’t try to reinvent networking — just makes it suck less. |
41 | 40 |
|
42 |
| -This isn’t an abstraction layer built on top of a ten-ton framework. It’s just clean, minimal UDP done right. |
43 |
| - |
44 |
| -## Usage |
| 41 | +## 🧑💻 Usage |
45 | 42 |
|
46 | 43 | ```cpp
|
47 | 44 | #include <pulse/net/udp/udp.h>
|
48 | 45 | #include <iostream>
|
| 46 | +#include <vector> |
49 | 47 |
|
50 | 48 | int main() {
|
51 |
| - pulse::net::udp::Addr serverAddr("127.0.0.1", 9000); |
52 |
| - auto server = pulse::net::udp::Listen(serverAddr); |
53 |
| - auto client = pulse::net::udp::Dial(serverAddr); |
| 49 | + using namespace pulse::net::udp; |
| 50 | + |
| 51 | + Addr serverAddr("127.0.0.1", 9000); |
54 | 52 |
|
55 |
| - std::vector<uint8_t> msg = {'h', 'e', 'l', 'l', 'o'}; |
56 |
| - client->send(msg); |
| 53 | + auto serverResult = Listen(serverAddr); |
| 54 | + if (!serverResult) { |
| 55 | + std::cerr << "Listen failed: " << static_cast<int>(serverResult.error()) << "\n"; |
| 56 | + return 1; |
| 57 | + } |
| 58 | + auto& server = *serverResult; |
57 | 59 |
|
58 |
| - std::optional<pulse::net::udp::Addr> from; |
59 |
| - std::vector<uint8_t> data; |
| 60 | + auto clientResult = Dial(serverAddr); |
| 61 | + if (!clientResult) { |
| 62 | + std::cerr << "Dial failed: " << static_cast<int>(clientResult.error()) << "\n"; |
| 63 | + return 1; |
| 64 | + } |
| 65 | + auto& client = *clientResult; |
60 | 66 |
|
61 |
| - if (auto maybe = server->recvFrom()) { |
62 |
| - std::tie(data, from) = *maybe; |
63 |
| - std::cout << "Got packet from " << from->ip << ":" << from->port << std::endl; |
| 67 | + std::vector<uint8_t> message = {'h', 'e', 'l', 'l', 'o'}; |
| 68 | + if (auto res = client->send(message.data(), message.size()); !res) { |
| 69 | + std::cerr << "Send failed: " << static_cast<int>(res.error()) << "\n"; |
| 70 | + return 1; |
64 | 71 | }
|
65 | 72 |
|
| 73 | + auto recvResult = server->recvFrom(); |
| 74 | + if (!recvResult) { |
| 75 | + std::cerr << "Receive failed: " << static_cast<int>(recvResult.error()) << "\n"; |
| 76 | + return 1; |
| 77 | + } |
| 78 | + |
| 79 | + const ReceivedPacket& packet = *recvResult; |
| 80 | + std::string msg(reinterpret_cast<const char*>(packet.data), packet.length); |
| 81 | + |
| 82 | + std::cout << "Received: " << msg << " from " << packet.addr.ip << ":" << packet.addr.port << "\n"; |
66 | 83 | return 0;
|
67 | 84 | }
|
68 | 85 | ```
|
69 | 86 |
|
70 |
| -## Platform Support |
| 87 | +## 🏗 Build Requirements |
| 88 | + |
| 89 | +* **C++23** |
| 90 | +* **CMake ≥ 3.15** |
| 91 | + |
| 92 | +If your compiler doesn’t support `std::expected`, upgrade. This is not a museum. |
| 93 | + |
| 94 | +### ⚠️ Error Handling Philosophy |
| 95 | + |
| 96 | +`pulse::net::udp` uses `std::expected` for all runtime operations. No exceptions are thrown during normal usage. |
| 97 | + |
| 98 | +The **only exceptions** are constructors like `Addr(ip, port)`, where C++ gives no sane way to return an error. If construction fails due to invalid input (e.g. garbage IP address), you'll get a `std::invalid_argument`. |
| 99 | + |
| 100 | +Everything else—`send()`, `recvFrom()`, `Dial()`, `Listen()`—uses `std::expected<T, ErrorCode>` so you can handle failures explicitly, without try/catch nonsense. |
| 101 | + |
| 102 | +## 📦 FetchContent |
| 103 | + |
| 104 | +You can pull in `pulse::net::udp` via `FetchContent` like this: |
| 105 | + |
| 106 | +```cmake |
| 107 | +include(FetchContent) |
| 108 | +
|
| 109 | +FetchContent_Declare( |
| 110 | + pulse_udp |
| 111 | + GIT_REPOSITORY https://git.pulsenet.dev/pulse/udp |
| 112 | + GIT_TAG v1.0.0 |
| 113 | +) |
| 114 | +
|
| 115 | +FetchContent_MakeAvailable(pulse_udp) |
| 116 | +
|
| 117 | +set(CMAKE_CXX_STANDARD 23) |
| 118 | +set(CMAKE_CXX_STANDARD_REQUIRED ON) |
| 119 | +
|
| 120 | +target_link_libraries(your_target PRIVATE pulse::net::udp) |
| 121 | +``` |
| 122 | + |
| 123 | +The actual target is aliased to `pulse::net::udp`, even though the library name is `pulsenet_udp`. |
| 124 | + |
| 125 | +## 🪟 Platform Support |
| 126 | + |
| 127 | +| Platform | Supported? | Notes | |
| 128 | +| -------- | ---------- | ----------------------------- | |
| 129 | +| Linux | ✅ | `fcntl()` for non-blocking | |
| 130 | +| macOS | ✅ | Same as above | |
| 131 | +| Windows | ✅ | Raw Winsock2 + `WSAStartup()` | |
| 132 | + |
| 133 | +## ⚖️ License |
71 | 134 |
|
72 |
| - | Platform | Supported? | Impl Notes | |
73 |
| - | --- | --- | --- | |
74 |
| - | macOS | ✅ | fcntl() non-blocking sockets | |
75 |
| - | Linux | ✅ | Same as above | |
76 |
| - | Windows | ✅ | Raw Winsock2 w/ WSAStartup() | |
| 135 | +**AGPLv3**. If that offends you, congratulations — it’s working as intended. |
77 | 136 |
|
78 |
| -You don’t need libuv, boost, or some “cross-platform networking abstraction” from hell. It’s all native. It’s all simple. |
| 137 | +Want to use this in a proprietary product? [Buy a commercial license](https://pulsenet.dev/) or go write your own UDP stack. |
79 | 138 |
|
80 |
| -## License |
| 139 | +## 🧨 Final Word |
81 | 140 |
|
82 |
| -AGPLv3. If you’re using this in a closed-source project, you need to license your project AGPLv3 as well or purchase a commercial license. |
| 141 | +This isn’t boost. This isn’t some academic networking playground. |
83 | 142 |
|
84 |
| -## Final Word |
| 143 | +If you want a fast, lean UDP layer that doesn’t try to abstract away the world — and doesn’t get in your way when you're building serious low-latency systems — you're in the right place. |
85 | 144 |
|
86 |
| -pulseudp is not a toy. It’s not trying to hold your hand. It gives you raw UDP, cleanly wrapped, so you can go build real-time systems without dragging 200 build dependencies behind you. |
| 145 | +If you need a coroutine DSL, TLS tunnels, and a metrics dashboard, leave now. |
87 | 146 |
|
88 |
| -If you want a networking library with 300 contributors and an official Discord server, you’re in the wrong place. |
| 147 | +## Version |
89 | 148 |
|
90 |
| -If you want simple, fast, understandable UDP for serious use — welcome. |
| 149 | +**pulse::net::udp v1.0.0** |
0 commit comments