appium-ios-tuntap 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/binding.gyp +19 -3
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/appium-ios-tuntap.node +0 -0
- package/prebuilds/darwin-x64/appium-ios-tuntap.node +0 -0
- package/src/native/handle.cc +59 -0
- package/src/native/handle.h +30 -0
- package/src/native/tun_backend.h +22 -2
- package/src/native/tun_backend_windows.cc +370 -0
- package/src/native/wintun_loader.cc +192 -0
- package/src/native/wintun_loader.h +93 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.3.0](https://github.com/appium/appium-ios-tuntap/compare/v0.2.5...v0.3.0) (2026-05-22)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* add Windows (WinTun) native backend ([#43](https://github.com/appium/appium-ios-tuntap/issues/43)) ([565b4c1](https://github.com/appium/appium-ios-tuntap/commit/565b4c1cfd2ddf4956ed32ade4f8208cd0d4f0f6)), closes [#ifdef](https://github.com/appium/appium-ios-tuntap/issues/ifdef)
|
|
6
|
+
|
|
1
7
|
## [0.2.5](https://github.com/appium/appium-ios-tuntap/compare/v0.2.4...v0.2.5) (2026-05-14)
|
|
2
8
|
|
|
3
9
|
### Code Refactoring
|
package/binding.gyp
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"-Wno-unused-parameter",
|
|
21
21
|
"-fPIC"
|
|
22
22
|
],
|
|
23
|
-
"cflags_cc": [
|
|
23
|
+
"cflags_cc": [
|
|
24
24
|
"-std=c++17",
|
|
25
25
|
"-Wno-vla-extension",
|
|
26
26
|
"-O3",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
]
|
|
49
49
|
},
|
|
50
50
|
"msvs_settings": {
|
|
51
|
-
"VCCLCompilerTool": {
|
|
51
|
+
"VCCLCompilerTool": {
|
|
52
52
|
"ExceptionHandling": 1,
|
|
53
53
|
"AdditionalOptions": [
|
|
54
54
|
"/std:c++17",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
]
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
|
-
"defines": [
|
|
59
|
+
"defines": [
|
|
60
60
|
"NAPI_CPP_EXCEPTIONS",
|
|
61
61
|
"NAPI_VERSION=8"
|
|
62
62
|
],
|
|
@@ -89,6 +89,22 @@
|
|
|
89
89
|
"-framework", "CoreFoundation"
|
|
90
90
|
]
|
|
91
91
|
}
|
|
92
|
+
}],
|
|
93
|
+
["OS=='win'", {
|
|
94
|
+
"sources": [
|
|
95
|
+
"src/native/handle.cc",
|
|
96
|
+
"src/native/wintun_loader.cc",
|
|
97
|
+
"src/native/tun_backend_windows.cc"
|
|
98
|
+
],
|
|
99
|
+
"libraries": [
|
|
100
|
+
"iphlpapi.lib",
|
|
101
|
+
"ws2_32.lib"
|
|
102
|
+
],
|
|
103
|
+
"defines": [
|
|
104
|
+
"_WIN32_WINNT=0x0A00",
|
|
105
|
+
"WIN32_LEAN_AND_MEAN",
|
|
106
|
+
"NOMINMAX"
|
|
107
|
+
]
|
|
92
108
|
}]
|
|
93
109
|
]
|
|
94
110
|
}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#ifdef _WIN32
|
|
2
|
+
|
|
3
|
+
#include "handle.h"
|
|
4
|
+
|
|
5
|
+
namespace {
|
|
6
|
+
|
|
7
|
+
bool IsRealHandle(HANDLE handle) {
|
|
8
|
+
return handle != nullptr && handle != INVALID_HANDLE_VALUE;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
} // namespace
|
|
12
|
+
|
|
13
|
+
Handle::Handle() : handle_(nullptr) {}
|
|
14
|
+
|
|
15
|
+
Handle::Handle(HANDLE handle) : handle_(handle) {}
|
|
16
|
+
|
|
17
|
+
Handle::~Handle() {
|
|
18
|
+
if (IsRealHandle(handle_)) {
|
|
19
|
+
::CloseHandle(handle_);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
Handle::Handle(Handle&& other) noexcept : handle_(other.handle_) {
|
|
24
|
+
other.handle_ = nullptr;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Handle& Handle::operator=(Handle&& other) noexcept {
|
|
28
|
+
if (this != &other) {
|
|
29
|
+
if (IsRealHandle(handle_)) {
|
|
30
|
+
::CloseHandle(handle_);
|
|
31
|
+
}
|
|
32
|
+
handle_ = other.handle_;
|
|
33
|
+
other.handle_ = nullptr;
|
|
34
|
+
}
|
|
35
|
+
return *this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
HANDLE Handle::get() const {
|
|
39
|
+
return handle_;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
HANDLE Handle::release() {
|
|
43
|
+
HANDLE temp = handle_;
|
|
44
|
+
handle_ = nullptr;
|
|
45
|
+
return temp;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
bool Handle::is_valid() const {
|
|
49
|
+
return IsRealHandle(handle_);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
void Handle::reset(HANDLE handle) {
|
|
53
|
+
if (IsRealHandle(handle_)) {
|
|
54
|
+
::CloseHandle(handle_);
|
|
55
|
+
}
|
|
56
|
+
handle_ = handle;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#endif
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#ifdef _WIN32
|
|
4
|
+
|
|
5
|
+
#include <windows.h>
|
|
6
|
+
|
|
7
|
+
// RAII wrapper for a Win32 `HANDLE`. Mirrors `FileDescriptor` so backends can
|
|
8
|
+
// rely on the same lifetime semantics regardless of OS.
|
|
9
|
+
class Handle {
|
|
10
|
+
public:
|
|
11
|
+
Handle();
|
|
12
|
+
explicit Handle(HANDLE handle);
|
|
13
|
+
~Handle();
|
|
14
|
+
|
|
15
|
+
Handle(const Handle&) = delete;
|
|
16
|
+
Handle& operator=(const Handle&) = delete;
|
|
17
|
+
|
|
18
|
+
Handle(Handle&& other) noexcept;
|
|
19
|
+
Handle& operator=(Handle&& other) noexcept;
|
|
20
|
+
|
|
21
|
+
HANDLE get() const;
|
|
22
|
+
HANDLE release();
|
|
23
|
+
bool is_valid() const;
|
|
24
|
+
void reset(HANDLE handle = nullptr);
|
|
25
|
+
|
|
26
|
+
private:
|
|
27
|
+
HANDLE handle_;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
#endif
|
package/src/native/tun_backend.h
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
|
-
#if !defined(__linux__) && !defined(__APPLE__)
|
|
4
|
-
#error "appium-ios-tuntap native addon supports only Linux and
|
|
3
|
+
#if !defined(__linux__) && !defined(__APPLE__) && !defined(_WIN32)
|
|
4
|
+
#error "appium-ios-tuntap native addon supports only Linux, macOS, and Windows"
|
|
5
5
|
#endif
|
|
6
6
|
|
|
7
7
|
#include <cstddef>
|
|
@@ -27,9 +27,25 @@ enum class ReadPacketStatus {
|
|
|
27
27
|
Error,
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Backend abstraction that hides OS-specific TUN device handling from the
|
|
32
|
+
* N-API surface.
|
|
33
|
+
*
|
|
34
|
+
* Each backend owns:
|
|
35
|
+
* - its native handle (POSIX file descriptor or Win32 `HANDLE`)
|
|
36
|
+
* - the receive-loop primitive it needs (libuv `uv_poll_t` on POSIX, a
|
|
37
|
+
* dedicated worker thread plus a Win32 event on Windows)
|
|
38
|
+
*/
|
|
30
39
|
class TunPlatformBackend {
|
|
31
40
|
public:
|
|
41
|
+
// Invoked once per packet read by the receive loop. Always called on a
|
|
42
|
+
// background thread (libuv loop thread on POSIX, worker thread on Windows);
|
|
43
|
+
// the caller in `tuntap.cc` is responsible for marshalling onto the JS
|
|
44
|
+
// thread via `Napi::ThreadSafeFunction`.
|
|
32
45
|
using PacketCallback = std::function<void(std::vector<uint8_t>)>;
|
|
46
|
+
|
|
47
|
+
// Invoked at most once when the receive loop encounters a fatal error and
|
|
48
|
+
// stops. The receive loop must not deliver any further packets afterwards.
|
|
33
49
|
using ErrorCallback = std::function<void(const std::string&)>;
|
|
34
50
|
|
|
35
51
|
virtual ~TunPlatformBackend() = default;
|
|
@@ -47,6 +63,8 @@ public:
|
|
|
47
63
|
size_t length,
|
|
48
64
|
std::string& error) = 0;
|
|
49
65
|
|
|
66
|
+
// Begin asynchronous packet delivery. `loop` is supplied by Node-API and is
|
|
67
|
+
// used by POSIX backends for `uv_poll_init`; Windows ignores it.
|
|
50
68
|
virtual bool StartReceiveLoop(uv_loop_t* loop,
|
|
51
69
|
size_t buffer_size,
|
|
52
70
|
PacketCallback on_packet,
|
|
@@ -54,6 +72,8 @@ public:
|
|
|
54
72
|
std::string& error) = 0;
|
|
55
73
|
virtual void StopReceiveLoop() = 0;
|
|
56
74
|
|
|
75
|
+
// Returns the underlying POSIX file descriptor when one exists. Backends
|
|
76
|
+
// without a numeric fd (e.g. Wintun on Windows) return `-1`.
|
|
57
77
|
virtual int GetNativeFd() const { return -1; }
|
|
58
78
|
};
|
|
59
79
|
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#ifdef _WIN32
|
|
2
|
+
|
|
3
|
+
#include "tun_backend.h"
|
|
4
|
+
|
|
5
|
+
#include <windows.h>
|
|
6
|
+
|
|
7
|
+
#include <atomic>
|
|
8
|
+
#include <chrono>
|
|
9
|
+
#include <cstring>
|
|
10
|
+
#include <sstream>
|
|
11
|
+
#include <thread>
|
|
12
|
+
#include <utility>
|
|
13
|
+
#include <vector>
|
|
14
|
+
|
|
15
|
+
#include "handle.h"
|
|
16
|
+
#include "wintun_loader.h"
|
|
17
|
+
|
|
18
|
+
namespace {
|
|
19
|
+
|
|
20
|
+
constexpr LPCWSTR kTunnelType = L"AppiumTunTap";
|
|
21
|
+
constexpr DWORD kSessionCapacity = 0x400000; // 4 MiB; must be power of two.
|
|
22
|
+
|
|
23
|
+
// WinTun adapter names are limited to MAX_ADAPTER_NAME-1 wide chars (~127).
|
|
24
|
+
// We stay well under that.
|
|
25
|
+
std::wstring BuildDefaultAdapterName() {
|
|
26
|
+
std::wostringstream oss;
|
|
27
|
+
oss << L"appium-tun" << ::GetCurrentProcessId() << L"-" << ::GetTickCount();
|
|
28
|
+
return oss.str();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class WindowsTunBackend : public TunPlatformBackend {
|
|
32
|
+
public:
|
|
33
|
+
WindowsTunBackend() = default;
|
|
34
|
+
~WindowsTunBackend() override {
|
|
35
|
+
StopReceiveLoop();
|
|
36
|
+
EndSessionInternal();
|
|
37
|
+
CloseAdapterInternal();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
WindowsTunBackend(const WindowsTunBackend&) = delete;
|
|
41
|
+
WindowsTunBackend& operator=(const WindowsTunBackend&) = delete;
|
|
42
|
+
WindowsTunBackend(WindowsTunBackend&&) = delete;
|
|
43
|
+
WindowsTunBackend& operator=(WindowsTunBackend&&) = delete;
|
|
44
|
+
|
|
45
|
+
bool OpenDevice(const std::string& requested_name,
|
|
46
|
+
std::string& out_interface_name,
|
|
47
|
+
std::string& error) override {
|
|
48
|
+
auto& api = WintunApi::Instance();
|
|
49
|
+
if (!api.Load(error)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
std::wstring adapter_name =
|
|
54
|
+
requested_name.empty() ? BuildDefaultAdapterName() : Utf8ToUtf16(requested_name);
|
|
55
|
+
if (adapter_name.empty()) {
|
|
56
|
+
error = "Failed to encode adapter name as UTF-16";
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Creating or opening a WinTun adapter requires the process to run with
|
|
61
|
+
// administrator privileges (elevated). Without elevation `CreateAdapter`
|
|
62
|
+
// fails with ERROR_ACCESS_DENIED, which `FormatLastError` surfaces in the
|
|
63
|
+
// error string below. This mirrors the root (EUID 0) requirement of the
|
|
64
|
+
// POSIX backends.
|
|
65
|
+
//
|
|
66
|
+
// Prefer creating a fresh adapter so `WintunCloseAdapter` on shutdown
|
|
67
|
+
// also tears down the kernel object. Falling back to `OpenAdapter`
|
|
68
|
+
// handles the "leftover from a crashed process" case where the adapter
|
|
69
|
+
// already exists; that adapter will likewise be removed on close, which
|
|
70
|
+
// is the desired behavior — we do not want stale adapters to accumulate.
|
|
71
|
+
adapter_ = api.CreateAdapter(adapter_name.c_str(), kTunnelType, nullptr);
|
|
72
|
+
if (!adapter_) {
|
|
73
|
+
DWORD created_err = ::GetLastError();
|
|
74
|
+
adapter_ = api.OpenAdapter(adapter_name.c_str());
|
|
75
|
+
if (!adapter_) {
|
|
76
|
+
DWORD opened_err = ::GetLastError();
|
|
77
|
+
error = "Failed to create or open WinTun adapter: create failed with " +
|
|
78
|
+
FormatLastError(created_err) + "; open failed with " +
|
|
79
|
+
FormatLastError(opened_err);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
session_ = api.StartSession(adapter_, kSessionCapacity);
|
|
85
|
+
if (!session_) {
|
|
86
|
+
error = "Failed to start WinTun session: " + FormatLastError(::GetLastError());
|
|
87
|
+
CloseAdapterInternal();
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
read_event_ = api.GetReadWaitEvent(session_);
|
|
92
|
+
if (!read_event_) {
|
|
93
|
+
error = "Failed to acquire WinTun read-wait event: " + FormatLastError(::GetLastError());
|
|
94
|
+
EndSessionInternal();
|
|
95
|
+
CloseAdapterInternal();
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface_name_ = Utf16ToUtf8(adapter_name);
|
|
100
|
+
if (interface_name_.empty()) {
|
|
101
|
+
error = "Failed to encode adapter name as UTF-8";
|
|
102
|
+
EndSessionInternal();
|
|
103
|
+
CloseAdapterInternal();
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
out_interface_name = interface_name_;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
void CloseDevice() override {
|
|
112
|
+
StopReceiveLoop();
|
|
113
|
+
EndSessionInternal();
|
|
114
|
+
CloseAdapterInternal();
|
|
115
|
+
interface_name_.clear();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
bool IsOpen() const override { return session_ != nullptr; }
|
|
119
|
+
|
|
120
|
+
ReadPacketStatus ReadPacket(size_t max_payload_size,
|
|
121
|
+
std::vector<uint8_t>& out,
|
|
122
|
+
std::string& error) override {
|
|
123
|
+
if (!session_) {
|
|
124
|
+
error = "Device not open";
|
|
125
|
+
return ReadPacketStatus::Error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
auto& api = WintunApi::Instance();
|
|
129
|
+
DWORD packet_size = 0;
|
|
130
|
+
BYTE* packet = api.ReceivePacket(session_, &packet_size);
|
|
131
|
+
if (!packet) {
|
|
132
|
+
DWORD err = ::GetLastError();
|
|
133
|
+
switch (err) {
|
|
134
|
+
case ERROR_NO_MORE_ITEMS:
|
|
135
|
+
out.clear();
|
|
136
|
+
return ReadPacketStatus::NoData;
|
|
137
|
+
case ERROR_HANDLE_EOF:
|
|
138
|
+
out.clear();
|
|
139
|
+
return ReadPacketStatus::Closed;
|
|
140
|
+
default:
|
|
141
|
+
out.clear();
|
|
142
|
+
error = "WintunReceivePacket failed: " + FormatLastError(err);
|
|
143
|
+
return ReadPacketStatus::Error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const size_t copy_len = static_cast<size_t>(packet_size) > max_payload_size
|
|
148
|
+
? max_payload_size
|
|
149
|
+
: static_cast<size_t>(packet_size);
|
|
150
|
+
out.assign(packet, packet + copy_len);
|
|
151
|
+
api.ReleaseReceivePacket(session_, packet);
|
|
152
|
+
return ReadPacketStatus::Data;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
ssize_t WritePacket(const uint8_t* data,
|
|
156
|
+
size_t length,
|
|
157
|
+
std::string& error) override {
|
|
158
|
+
if (!session_) {
|
|
159
|
+
error = "Device not open";
|
|
160
|
+
return -1;
|
|
161
|
+
}
|
|
162
|
+
if (length == 0) {
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
if (length > WINTUN_MAX_IP_PACKET_SIZE) {
|
|
166
|
+
error = "Packet exceeds WINTUN_MAX_IP_PACKET_SIZE";
|
|
167
|
+
return -1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
auto& api = WintunApi::Instance();
|
|
171
|
+
BYTE* slot = api.AllocateSendPacket(session_, static_cast<DWORD>(length));
|
|
172
|
+
if (!slot) {
|
|
173
|
+
DWORD err = ::GetLastError();
|
|
174
|
+
if (err == ERROR_HANDLE_EOF) {
|
|
175
|
+
error = "WinTun adapter is terminating";
|
|
176
|
+
} else if (err == ERROR_BUFFER_OVERFLOW) {
|
|
177
|
+
error = "WinTun send-ring is full";
|
|
178
|
+
} else {
|
|
179
|
+
error = "WintunAllocateSendPacket failed: " + FormatLastError(err);
|
|
180
|
+
}
|
|
181
|
+
return -1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
std::memcpy(slot, data, length);
|
|
185
|
+
api.SendPacket(session_, slot);
|
|
186
|
+
return static_cast<ssize_t>(length);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The worker thread can begin invoking `on_packet`/`on_error` immediately
|
|
190
|
+
// after this returns; callers must therefore not assume callbacks fire
|
|
191
|
+
// only after the function returns. In practice the callbacks defined in
|
|
192
|
+
// `tuntap.cc` either marshal to libuv via TSFN (`on_packet`) or take
|
|
193
|
+
// `device_mutex_` (`on_error`). Brief contention on `device_mutex_` is
|
|
194
|
+
// expected and resolves on the order of microseconds because
|
|
195
|
+
// `std::thread`'s constructor does not block on the worker reaching its
|
|
196
|
+
// first instruction. There is no deadlock risk because the calling JS
|
|
197
|
+
// thread releases the lock as soon as `StartPolling` returns.
|
|
198
|
+
bool StartReceiveLoop(uv_loop_t* /*loop*/,
|
|
199
|
+
size_t buffer_size,
|
|
200
|
+
PacketCallback on_packet,
|
|
201
|
+
ErrorCallback on_error,
|
|
202
|
+
std::string& error) override {
|
|
203
|
+
if (!session_) {
|
|
204
|
+
error = "Device not open";
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (buffer_size == 0) {
|
|
208
|
+
error = "Invalid receive-loop parameters";
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
if (worker_running_.load()) {
|
|
212
|
+
error = "Receive loop already started";
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
quit_event_.reset(::CreateEventW(nullptr, /*manualReset=*/TRUE,
|
|
217
|
+
/*initialState=*/FALSE, nullptr));
|
|
218
|
+
if (!quit_event_.is_valid()) {
|
|
219
|
+
error = "Failed to create quit event: " + FormatLastError(::GetLastError());
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
worker_running_.store(true);
|
|
224
|
+
try {
|
|
225
|
+
worker_ = std::thread(&WindowsTunBackend::WorkerMain, this, buffer_size,
|
|
226
|
+
std::move(on_packet), std::move(on_error));
|
|
227
|
+
} catch (const std::system_error& sysErr) {
|
|
228
|
+
worker_running_.store(false);
|
|
229
|
+
quit_event_.reset();
|
|
230
|
+
error = std::string("Failed to spawn receive thread: ") + sysErr.what();
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
void StopReceiveLoop() override {
|
|
237
|
+
if (!worker_.joinable()) {
|
|
238
|
+
// Either never started or already cleaned up. Reset the event in case
|
|
239
|
+
// it was created without ever spawning a thread.
|
|
240
|
+
quit_event_.reset();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
worker_running_.store(false);
|
|
244
|
+
if (quit_event_.is_valid()) {
|
|
245
|
+
::SetEvent(quit_event_.get());
|
|
246
|
+
}
|
|
247
|
+
worker_.join();
|
|
248
|
+
quit_event_.reset();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// WinTun exposes no POSIX file descriptor: its readable object is a Win32
|
|
252
|
+
// event `HANDLE`, not a numeric fd. Always -1 — the N-API layer treats -1
|
|
253
|
+
// as "no pollable fd" and drives delivery through `StartReceiveLoop`.
|
|
254
|
+
int GetNativeFd() const override { return -1; }
|
|
255
|
+
|
|
256
|
+
private:
|
|
257
|
+
static std::string Utf16ToUtf8(const std::wstring& utf16) {
|
|
258
|
+
if (utf16.empty()) {
|
|
259
|
+
return std::string();
|
|
260
|
+
}
|
|
261
|
+
int len = ::WideCharToMultiByte(CP_UTF8, 0, utf16.c_str(),
|
|
262
|
+
static_cast<int>(utf16.size()), nullptr, 0,
|
|
263
|
+
nullptr, nullptr);
|
|
264
|
+
if (len <= 0) {
|
|
265
|
+
return std::string();
|
|
266
|
+
}
|
|
267
|
+
std::string out(static_cast<size_t>(len), '\0');
|
|
268
|
+
::WideCharToMultiByte(CP_UTF8, 0, utf16.c_str(),
|
|
269
|
+
static_cast<int>(utf16.size()), out.data(), len,
|
|
270
|
+
nullptr, nullptr);
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
void EndSessionInternal() {
|
|
275
|
+
if (session_) {
|
|
276
|
+
WintunApi::Instance().EndSession(session_);
|
|
277
|
+
session_ = nullptr;
|
|
278
|
+
}
|
|
279
|
+
read_event_ = nullptr;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
void CloseAdapterInternal() {
|
|
283
|
+
if (adapter_) {
|
|
284
|
+
WintunApi::Instance().CloseAdapter(adapter_);
|
|
285
|
+
adapter_ = nullptr;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
void WorkerMain(size_t buffer_size,
|
|
290
|
+
PacketCallback on_packet,
|
|
291
|
+
ErrorCallback on_error) {
|
|
292
|
+
auto& api = WintunApi::Instance();
|
|
293
|
+
HANDLE wait_handles[2] = {read_event_, quit_event_.get()};
|
|
294
|
+
|
|
295
|
+
while (worker_running_.load()) {
|
|
296
|
+
// Drain everything available before going back to wait. WinTun's
|
|
297
|
+
// read-wait event is auto-reset on signal, so we must consume all
|
|
298
|
+
// queued packets before re-arming.
|
|
299
|
+
bool drained = false;
|
|
300
|
+
while (worker_running_.load()) {
|
|
301
|
+
DWORD packet_size = 0;
|
|
302
|
+
BYTE* packet = api.ReceivePacket(session_, &packet_size);
|
|
303
|
+
if (packet) {
|
|
304
|
+
const size_t copy_len = static_cast<size_t>(packet_size) > buffer_size
|
|
305
|
+
? buffer_size
|
|
306
|
+
: static_cast<size_t>(packet_size);
|
|
307
|
+
std::vector<uint8_t> data(packet, packet + copy_len);
|
|
308
|
+
api.ReleaseReceivePacket(session_, packet);
|
|
309
|
+
if (on_packet) {
|
|
310
|
+
on_packet(std::move(data));
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
DWORD err = ::GetLastError();
|
|
316
|
+
if (err == ERROR_NO_MORE_ITEMS) {
|
|
317
|
+
drained = true;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
if (err == ERROR_HANDLE_EOF) {
|
|
321
|
+
if (on_error) {
|
|
322
|
+
on_error("WinTun adapter terminating");
|
|
323
|
+
}
|
|
324
|
+
worker_running_.store(false);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (on_error) {
|
|
328
|
+
on_error("WintunReceivePacket failed: " + FormatLastError(err));
|
|
329
|
+
}
|
|
330
|
+
worker_running_.store(false);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!worker_running_.load()) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (!drained) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
DWORD wait = ::WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE);
|
|
342
|
+
if (wait == WAIT_OBJECT_0 + 1) {
|
|
343
|
+
// quit_event signalled.
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (wait != WAIT_OBJECT_0) {
|
|
347
|
+
if (on_error) {
|
|
348
|
+
on_error("WaitForMultipleObjects failed: " + FormatLastError(::GetLastError()));
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
WINTUN_ADAPTER_HANDLE adapter_ = nullptr;
|
|
356
|
+
WINTUN_SESSION_HANDLE session_ = nullptr;
|
|
357
|
+
HANDLE read_event_ = nullptr; // Owned by `session_`; do not CloseHandle.
|
|
358
|
+
Handle quit_event_;
|
|
359
|
+
std::thread worker_;
|
|
360
|
+
std::atomic<bool> worker_running_{false};
|
|
361
|
+
std::string interface_name_;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
} // namespace
|
|
365
|
+
|
|
366
|
+
std::unique_ptr<TunPlatformBackend> CreatePlatformBackend() {
|
|
367
|
+
return std::make_unique<WindowsTunBackend>();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#endif
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#ifdef _WIN32
|
|
2
|
+
|
|
3
|
+
#include "wintun_loader.h"
|
|
4
|
+
|
|
5
|
+
#include <vector>
|
|
6
|
+
|
|
7
|
+
namespace {
|
|
8
|
+
|
|
9
|
+
constexpr LPCWSTR kWintunDllName = L"wintun.dll";
|
|
10
|
+
|
|
11
|
+
// Returns the directory that contains the addon module that this code is
|
|
12
|
+
// linked into. Used to locate `wintun.dll` shipped next to `tuntap.node`.
|
|
13
|
+
std::wstring GetAddonDirectory() {
|
|
14
|
+
HMODULE module = nullptr;
|
|
15
|
+
// Pass the address of any function in this translation unit so the resolver
|
|
16
|
+
// returns the addon's own module rather than the host process executable.
|
|
17
|
+
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
|
18
|
+
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
|
19
|
+
reinterpret_cast<LPCWSTR>(&GetAddonDirectory),
|
|
20
|
+
&module)) {
|
|
21
|
+
return std::wstring();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
std::vector<wchar_t> buffer(MAX_PATH);
|
|
25
|
+
for (;;) {
|
|
26
|
+
DWORD len = GetModuleFileNameW(module, buffer.data(),
|
|
27
|
+
static_cast<DWORD>(buffer.size()));
|
|
28
|
+
if (len == 0) {
|
|
29
|
+
return std::wstring();
|
|
30
|
+
}
|
|
31
|
+
if (len < buffer.size()) {
|
|
32
|
+
buffer.resize(len);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
buffer.resize(buffer.size() * 2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
std::wstring path(buffer.begin(), buffer.end());
|
|
39
|
+
size_t pos = path.find_last_of(L"\\/");
|
|
40
|
+
if (pos == std::wstring::npos) {
|
|
41
|
+
return std::wstring();
|
|
42
|
+
}
|
|
43
|
+
path.resize(pos);
|
|
44
|
+
return path;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
template <typename T>
|
|
48
|
+
bool Resolve(HMODULE module, const char* name, T& out, std::string& error) {
|
|
49
|
+
out = reinterpret_cast<T>(::GetProcAddress(module, name));
|
|
50
|
+
if (!out) {
|
|
51
|
+
error = std::string("Failed to resolve wintun.dll export: ") + name;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
} // namespace
|
|
58
|
+
|
|
59
|
+
WintunApi& WintunApi::Instance() {
|
|
60
|
+
static WintunApi instance;
|
|
61
|
+
return instance;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
bool WintunApi::Load(std::string& error) {
|
|
65
|
+
std::lock_guard<std::mutex> lock(load_mutex_);
|
|
66
|
+
if (loaded_) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Try addon-directory first so `wintun.dll` shipped next to `tuntap.node`
|
|
71
|
+
// wins over a stale system copy.
|
|
72
|
+
std::wstring addon_dir = GetAddonDirectory();
|
|
73
|
+
if (!addon_dir.empty()) {
|
|
74
|
+
std::wstring local = addon_dir + L"\\" + kWintunDllName;
|
|
75
|
+
if (TryLoadFrom(local.c_str())) {
|
|
76
|
+
return ResolveEntryPoints(error);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fall back to the OS-default search list (Application dir, System32, …)
|
|
81
|
+
// by passing only the file name.
|
|
82
|
+
if (TryLoadFrom(kWintunDllName)) {
|
|
83
|
+
return ResolveEntryPoints(error);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
error = std::string("Failed to load wintun.dll: ") + FormatLastError(::GetLastError()) +
|
|
87
|
+
". Make sure wintun.dll is shipped next to tuntap.node or installed system-wide.";
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
bool WintunApi::TryLoadFrom(LPCWSTR path) {
|
|
92
|
+
// `LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR` lets a DLL loaded from an explicit
|
|
93
|
+
// path resolve its own dependencies from its own directory. WinTun has
|
|
94
|
+
// no third-party dependencies today, but this is the documented-safe
|
|
95
|
+
// flag set for loading by absolute path and is free to include.
|
|
96
|
+
module_ = ::LoadLibraryExW(path, nullptr,
|
|
97
|
+
LOAD_LIBRARY_SEARCH_APPLICATION_DIR |
|
|
98
|
+
LOAD_LIBRARY_SEARCH_SYSTEM32 |
|
|
99
|
+
LOAD_LIBRARY_SEARCH_USER_DIRS |
|
|
100
|
+
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR);
|
|
101
|
+
return module_ != nullptr;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
bool WintunApi::ResolveEntryPoints(std::string& error) {
|
|
105
|
+
if (!module_) {
|
|
106
|
+
error = "wintun.dll module handle is null";
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!Resolve(module_, "WintunCreateAdapter", CreateAdapter, error) ||
|
|
111
|
+
!Resolve(module_, "WintunOpenAdapter", OpenAdapter, error) ||
|
|
112
|
+
!Resolve(module_, "WintunCloseAdapter", CloseAdapter, error) ||
|
|
113
|
+
!Resolve(module_, "WintunGetRunningDriverVersion", GetRunningDriverVersion, error) ||
|
|
114
|
+
!Resolve(module_, "WintunStartSession", StartSession, error) ||
|
|
115
|
+
!Resolve(module_, "WintunEndSession", EndSession, error) ||
|
|
116
|
+
!Resolve(module_, "WintunGetReadWaitEvent", GetReadWaitEvent, error) ||
|
|
117
|
+
!Resolve(module_, "WintunReceivePacket", ReceivePacket, error) ||
|
|
118
|
+
!Resolve(module_, "WintunReleaseReceivePacket", ReleaseReceivePacket, error) ||
|
|
119
|
+
!Resolve(module_, "WintunAllocateSendPacket", AllocateSendPacket, error) ||
|
|
120
|
+
!Resolve(module_, "WintunSendPacket", SendPacket, error)) {
|
|
121
|
+
// Some pointers may already have been resolved before the failure.
|
|
122
|
+
// Null everything so callers cannot read stale addresses into a
|
|
123
|
+
// module that we are about to FreeLibrary.
|
|
124
|
+
ClearEntryPoints();
|
|
125
|
+
::FreeLibrary(module_);
|
|
126
|
+
module_ = nullptr;
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
loaded_ = true;
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
void WintunApi::ClearEntryPoints() {
|
|
135
|
+
CreateAdapter = nullptr;
|
|
136
|
+
OpenAdapter = nullptr;
|
|
137
|
+
CloseAdapter = nullptr;
|
|
138
|
+
GetRunningDriverVersion = nullptr;
|
|
139
|
+
StartSession = nullptr;
|
|
140
|
+
EndSession = nullptr;
|
|
141
|
+
GetReadWaitEvent = nullptr;
|
|
142
|
+
ReceivePacket = nullptr;
|
|
143
|
+
ReleaseReceivePacket = nullptr;
|
|
144
|
+
AllocateSendPacket = nullptr;
|
|
145
|
+
SendPacket = nullptr;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
std::string FormatLastError(DWORD error_code) {
|
|
149
|
+
if (error_code == 0) {
|
|
150
|
+
return "no error";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
LPSTR buffer = nullptr;
|
|
154
|
+
DWORD len = ::FormatMessageA(
|
|
155
|
+
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
|
|
156
|
+
FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
157
|
+
nullptr, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
|
158
|
+
reinterpret_cast<LPSTR>(&buffer), 0, nullptr);
|
|
159
|
+
|
|
160
|
+
std::string message;
|
|
161
|
+
if (len && buffer) {
|
|
162
|
+
message.assign(buffer, len);
|
|
163
|
+
::LocalFree(buffer);
|
|
164
|
+
while (!message.empty() && (message.back() == '\n' || message.back() == '\r')) {
|
|
165
|
+
message.pop_back();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (message.empty()) {
|
|
170
|
+
message = "Unknown error";
|
|
171
|
+
}
|
|
172
|
+
return message + " (code=" + std::to_string(error_code) + ")";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
std::wstring Utf8ToUtf16(const std::string& utf8) {
|
|
176
|
+
if (utf8.empty()) {
|
|
177
|
+
return std::wstring();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
int len = ::MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(),
|
|
181
|
+
static_cast<int>(utf8.size()), nullptr, 0);
|
|
182
|
+
if (len <= 0) {
|
|
183
|
+
return std::wstring();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
std::wstring result(static_cast<size_t>(len), L'\0');
|
|
187
|
+
::MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(),
|
|
188
|
+
static_cast<int>(utf8.size()), result.data(), len);
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#endif
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#ifdef _WIN32
|
|
4
|
+
|
|
5
|
+
#include <windows.h>
|
|
6
|
+
#include <ifdef.h>
|
|
7
|
+
|
|
8
|
+
#include <mutex>
|
|
9
|
+
#include <string>
|
|
10
|
+
|
|
11
|
+
// Function-pointer typedefs reproduced from the WireGuard-supplied wintun.h
|
|
12
|
+
// (https://git.zx2c4.com/wintun/plain/api/wintun.h, GPL-2.0 OR MIT). Only the
|
|
13
|
+
// pieces we use are declared here so we do not have to vendor the full
|
|
14
|
+
// upstream header.
|
|
15
|
+
|
|
16
|
+
typedef struct _WINTUN_ADAPTER* WINTUN_ADAPTER_HANDLE;
|
|
17
|
+
typedef struct _TUN_SESSION* WINTUN_SESSION_HANDLE;
|
|
18
|
+
|
|
19
|
+
#ifndef WINTUN_MIN_RING_CAPACITY
|
|
20
|
+
#define WINTUN_MIN_RING_CAPACITY 0x20000
|
|
21
|
+
#endif
|
|
22
|
+
#ifndef WINTUN_MAX_RING_CAPACITY
|
|
23
|
+
#define WINTUN_MAX_RING_CAPACITY 0x4000000
|
|
24
|
+
#endif
|
|
25
|
+
#ifndef WINTUN_MAX_IP_PACKET_SIZE
|
|
26
|
+
#define WINTUN_MAX_IP_PACKET_SIZE 0xFFFF
|
|
27
|
+
#endif
|
|
28
|
+
|
|
29
|
+
typedef WINTUN_ADAPTER_HANDLE(WINAPI* WINTUN_CREATE_ADAPTER_FUNC)(LPCWSTR Name,
|
|
30
|
+
LPCWSTR TunnelType,
|
|
31
|
+
const GUID* RequestedGUID);
|
|
32
|
+
typedef WINTUN_ADAPTER_HANDLE(WINAPI* WINTUN_OPEN_ADAPTER_FUNC)(LPCWSTR Name);
|
|
33
|
+
typedef VOID(WINAPI* WINTUN_CLOSE_ADAPTER_FUNC)(WINTUN_ADAPTER_HANDLE Adapter);
|
|
34
|
+
typedef BOOL(WINAPI* WINTUN_DELETE_DRIVER_FUNC)(VOID);
|
|
35
|
+
typedef VOID(WINAPI* WINTUN_GET_ADAPTER_LUID_FUNC)(WINTUN_ADAPTER_HANDLE Adapter, NET_LUID* Luid);
|
|
36
|
+
typedef DWORD(WINAPI* WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC)(VOID);
|
|
37
|
+
|
|
38
|
+
typedef WINTUN_SESSION_HANDLE(WINAPI* WINTUN_START_SESSION_FUNC)(WINTUN_ADAPTER_HANDLE Adapter,
|
|
39
|
+
DWORD Capacity);
|
|
40
|
+
typedef VOID(WINAPI* WINTUN_END_SESSION_FUNC)(WINTUN_SESSION_HANDLE Session);
|
|
41
|
+
typedef HANDLE(WINAPI* WINTUN_GET_READ_WAIT_EVENT_FUNC)(WINTUN_SESSION_HANDLE Session);
|
|
42
|
+
typedef BYTE*(WINAPI* WINTUN_RECEIVE_PACKET_FUNC)(WINTUN_SESSION_HANDLE Session, DWORD* PacketSize);
|
|
43
|
+
typedef VOID(WINAPI* WINTUN_RELEASE_RECEIVE_PACKET_FUNC)(WINTUN_SESSION_HANDLE Session,
|
|
44
|
+
const BYTE* Packet);
|
|
45
|
+
typedef BYTE*(WINAPI* WINTUN_ALLOCATE_SEND_PACKET_FUNC)(WINTUN_SESSION_HANDLE Session,
|
|
46
|
+
DWORD PacketSize);
|
|
47
|
+
typedef VOID(WINAPI* WINTUN_SEND_PACKET_FUNC)(WINTUN_SESSION_HANDLE Session, const BYTE* Packet);
|
|
48
|
+
|
|
49
|
+
// Singleton container for the resolved entry points. Callers must invoke
|
|
50
|
+
// `Load` (returning false on failure) before reading any function pointer.
|
|
51
|
+
class WintunApi {
|
|
52
|
+
public:
|
|
53
|
+
static WintunApi& Instance();
|
|
54
|
+
|
|
55
|
+
// Loads `wintun.dll` and resolves all required entry points. On the first
|
|
56
|
+
// failure `error` describes which step went wrong; subsequent calls succeed
|
|
57
|
+
// immediately as long as a previous call already loaded the library.
|
|
58
|
+
bool Load(std::string& error);
|
|
59
|
+
|
|
60
|
+
WINTUN_CREATE_ADAPTER_FUNC CreateAdapter = nullptr;
|
|
61
|
+
WINTUN_OPEN_ADAPTER_FUNC OpenAdapter = nullptr;
|
|
62
|
+
WINTUN_CLOSE_ADAPTER_FUNC CloseAdapter = nullptr;
|
|
63
|
+
WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC GetRunningDriverVersion = nullptr;
|
|
64
|
+
WINTUN_START_SESSION_FUNC StartSession = nullptr;
|
|
65
|
+
WINTUN_END_SESSION_FUNC EndSession = nullptr;
|
|
66
|
+
WINTUN_GET_READ_WAIT_EVENT_FUNC GetReadWaitEvent = nullptr;
|
|
67
|
+
WINTUN_RECEIVE_PACKET_FUNC ReceivePacket = nullptr;
|
|
68
|
+
WINTUN_RELEASE_RECEIVE_PACKET_FUNC ReleaseReceivePacket = nullptr;
|
|
69
|
+
WINTUN_ALLOCATE_SEND_PACKET_FUNC AllocateSendPacket = nullptr;
|
|
70
|
+
WINTUN_SEND_PACKET_FUNC SendPacket = nullptr;
|
|
71
|
+
|
|
72
|
+
private:
|
|
73
|
+
WintunApi() = default;
|
|
74
|
+
WintunApi(const WintunApi&) = delete;
|
|
75
|
+
WintunApi& operator=(const WintunApi&) = delete;
|
|
76
|
+
|
|
77
|
+
bool TryLoadFrom(LPCWSTR path);
|
|
78
|
+
bool ResolveEntryPoints(std::string& error);
|
|
79
|
+
void ClearEntryPoints();
|
|
80
|
+
|
|
81
|
+
std::mutex load_mutex_;
|
|
82
|
+
HMODULE module_ = nullptr;
|
|
83
|
+
bool loaded_ = false;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Builds a UTF-8 description of the latest Win32 error suitable for embedding
|
|
87
|
+
// in `std::string` error messages.
|
|
88
|
+
std::string FormatLastError(DWORD error_code);
|
|
89
|
+
|
|
90
|
+
// Converts a UTF-8 string to UTF-16 for Win32 wide-char APIs.
|
|
91
|
+
std::wstring Utf8ToUtf16(const std::string& utf8);
|
|
92
|
+
|
|
93
|
+
#endif
|