Windows API 深度解析
上一篇:WindowsAPI|每天了解几个winAPI接口之网络配置相关文档Iphlpapi.h详细分析14
本文继续对 Windows 网络接口相关的时间戳管理 API 进行解析,科普与参考为主,如有错误欢迎指正。
C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\um\iphlpapi.h
文章目录
- [GetIpErrorString:将 IP_STATUS 错误码转为可读字符串](#GetIpErrorString:将 IP_STATUS 错误码转为可读字符串)
- [ResolveNeighbor:解析邻居的物理地址(ARP/ND 类接口)](#ResolveNeighbor:解析邻居的物理地址(ARP/ND 类接口))
- [CreatePersistentTcpPortReservation:在系统内部"锁定"一段 TCP 端口](#CreatePersistentTcpPortReservation:在系统内部“锁定”一段 TCP 端口)
-
- 参数解释
- 常见返回值(失败原因说明)
- [小型可编译示例(C / Win32)](#小型可编译示例(C / Win32))
- 使用注意点
- [CreatePersistentUdpPortReservation:UDP 持久端口预留](#CreatePersistentUdpPortReservation:UDP 持久端口预留)
- [DeletePersistentTcpPortReservation:删除 TCP 持久端口预留](#DeletePersistentTcpPortReservation:删除 TCP 持久端口预留)
- [DeletePersistentUdpPortReservation:删除 UDP 持久端口预留](#DeletePersistentUdpPortReservation:删除 UDP 持久端口预留)
- [LookupPersistentTcpPortReservation:查询 TCP 持久端口预留](#LookupPersistentTcpPortReservation:查询 TCP 持久端口预留)
- [LookupPersistentUdpPortReservation:查询一段 UDP 端口区间是否已经被系统做了持久端口保留](#LookupPersistentUdpPortReservation:查询一段 UDP 端口区间是否已经被系统做了持久端口保留)
GetIpErrorString:将 IP_STATUS 错误码转为可读字符串
cpp
#if (NTDDI_VERSION >= NTDDI_VISTA)
IPHLPAPI_DLL_LINKAGE
DWORD
WINAPI
GetIpErrorString(
_In_ IP_STATUS ErrorCode,
_Out_writes_opt_(*Size + 1) PWSTR Buffer,
_Inout_ PDWORD Size
);
GetIpErrorString 用于将 IP Helper API 或 ICMP 相关函数返回的 IP_STATUS 错误码转换成更友好的文本描述。典型错误例如 IP_REQ_TIMED_OUT、IP_BAD_ROUTE 等。使用该函数可以在日志或调试输出中得到更可读的错误解释。
参数说明
ErrorCode
需要转换的 IP 错误码,通常来自 ICMP 或 IP Helper 返回值,例如 IcmpSendEcho、GetBestRoute 等。
Buffer
输出缓冲区,用于存放 Unicode 字符串。如果传入 nullptr,函数会将所需空间大小写入 Size 中。
Size
输入输出参数。
输入:Buffer 的大小(以 WCHAR 数量计)。
输出:实际写入(或所需)的大小。
返回值说明
返回 DWORD:
- NO_ERROR (0):成功将错误码转换为可读字符串。
- ERROR_INSUFFICIENT_BUFFER :
Buffer不够大,需要将Size调整为给定值后重新调用。 - ERROR_INVALID_PARAMETER:传参无效或结构体未正确初始化。
- 其他错误码 :可能表示系统不识别该
IP_STATUS。
简易示例
cpp
#include <windows.h>
#include <iphlpapi.h>
#include <iostream>
#pragma comment(lib, "iphlpapi.lib")
int main()
{
DWORD size = 0;
// 第一次调用:获取所需长度
GetIpErrorString(IP_REQ_TIMED_OUT, nullptr, &size);
std::wstring buf(size, L'\0');
DWORD ret = GetIpErrorString(IP_REQ_TIMED_OUT, buf.data(), &size);
if (ret == NO_ERROR) {
std::wcout << L"Error string: " << buf.c_str() << std::endl;
}
return 0;
}
ResolveNeighbor:解析邻居的物理地址(ARP/ND 类接口)
cpp
#if (NTDDI_VERSION >= NTDDI_VISTA)
#ifdef _WS2DEF_
IPHLPAPI_DLL_LINKAGE
ULONG
WINAPI
ResolveNeighbor(
_In_ SOCKADDR *NetworkAddress,
_Out_writes_bytes_(*PhysicalAddressLength) PVOID PhysicalAddress,
_Inout_ PULONG PhysicalAddressLength
);
#endif
#endif
ResolveNeighbor 是 Windows Vista 及之后提供的 API,用于直接解析某个网络地址(IPv4 或 IPv6)对应的物理地址(MAC),类似于底层的 ARP(IPv4)或 邻居发现 ND(IPv6)。
它比传统的 SendARP 更通用,可同时适用于 IPv4 和 IPv6,并使用现代的 SOCKADDR 结构。
参数说明
NetworkAddress
输入网络地址,必须是一个完整设置了 family、port、addr 的 SOCKADDR_IN(IPv4)或 SOCKADDR_IN6(IPv6)。
PhysicalAddress
输出目标的物理地址缓冲区,可以是 6 字节(MAC)、8 字节(某些虚拟接口)、或更长。
必须由调用者提供,系统只写入数据。
PhysicalAddressLength
输入:指定 PhysicalAddress 的最大容量。
输出:成功时为实际使用的字节数。若空间不够,返回 ERROR_INSUFFICIENT_BUFFER,并告知所需大小。
返回值说明
返回 ULONG:
- NO_ERROR (0):成功解析到邻居 MAC。
- ERROR_NOT_FOUND:邻居不存在(ARP 未命中,或系统没有建立 ND 条目)。
- ERROR_INSUFFICIENT_BUFFER:缓冲区容量不足。
- ERROR_INVALID_PARAMETER:输入的地址结构不正确。
- ERROR_GEN_FAILURE / ERROR_BAD_NET_NAME:网络不可达或接口不匹配。
该 API 不会强制发送 ARP 请求,它通常依赖系统已有的邻居缓存。如果缓存中不存在,结果可能是没找到。
简易示例:解析 IPv4 地址的 MAC
cpp
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include <iostream>
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")
int main()
{
sockaddr_in addr = {};
addr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr);
BYTE mac[32] = {0};
ULONG macLen = sizeof(mac);
ULONG ret = ResolveNeighbor((SOCKADDR*)&addr, mac, &macLen);
if (ret == NO_ERROR) {
std::cout << "MAC: ";
for (ULONG i = 0; i < macLen; ++i)
std::cout << std::hex << (int)mac[i] << " ";
std::cout << std::endl;
} else {
std::cout << "ResolveNeighbor failed. Code = " << ret << "\n";
}
return 0;
}
CreatePersistentTcpPortReservation:在系统内部"锁定"一段 TCP 端口
cpp
IPHLPAPI_DLL_LINKAGE
ULONG
WINAPI
CreatePersistentTcpPortReservation(
_In_ USHORT StartPort,
_In_ USHORT NumberOfPorts,
_Out_ PULONG64 Token
);
这是 Windows TCP Stack 提供的持久端口预留 API。用于在系统内部"锁定"一段 TCP 端口,使得:
- 其他进程不能随意 Bind 这些端口
- 系统不会把它们当作动态端口分配
- 预留信息写入注册表,在重启后仍然生效
适用于需要保证端口长期可用的服务(VPN、专网组件、内核态驱动等)。
参数解释
-
StartPort
你要预留的起始 TCP 端口(必须是主机字节序)。
-
NumberOfPorts
预留多少连续端口。例如 StartPort=5000, NumberOfPorts=10 → 5000--5009。
-
Token
输出的"预留块 Token",实际上是注册表内部的一种标识。
删除预留 要用 Token 调用DeletePersistentTcpPortReservation()。
常见返回值(失败原因说明)
返回值是 ULONG,等同于 Win32 Error Code。
常见错误列表:
| 返回值 | 含义 | 说明 |
|---|---|---|
| NO_ERROR (0) | 成功 | |
| ERROR_ACCESS_DENIED (5) | 权限不足 | 需要管理员权限 |
| ERROR_INVALID_PARAMETER (87) | 参数错误 | 端口范围错误或 NumberOfPorts 为 0 |
| ERROR_ALREADY_EXISTS (183) | 端口已被预留 | 系统里已存在同样范围 |
| ERROR_INVALID_DATA (13) | 数据无效 | Token == NULL |
| ERROR_NOT_ENOUGH_MEMORY (8) | 资源不足 | 非常少见 |
| ERROR_SHARING_VIOLATION (32) | 端口已占用 | 其他服务已 bind or 保留 |
小型可编译示例(C / Win32)
你可以保存成 reserve.c 并用 cl reserve.c /link iphlpapi.lib 编译。
c
#include <windows.h>
#include <stdio.h>
#include <iphlpapi.h>
#pragma comment(lib, "iphlpapi.lib")
int main() {
USHORT startPort = 5000;
USHORT count = 5;
ULONG64 token = 0;
ULONG ret = CreatePersistentTcpPortReservation(startPort, count, &token);
if (ret == NO_ERROR) {
printf("Port reservation created.\n");
printf("Start: %u Count: %u Token: %llu\n",
startPort, count, token);
} else {
printf("CreatePersistentTcpPortReservation failed: %lu\n", ret);
return 1;
}
// 想删除预留,使用 DeletePersistentTcpPortReservation
ULONG ret2 = DeletePersistentTcpPortReservation(startPort, count, token);
if (ret2 == NO_ERROR) {
printf("Reservation removed.\n");
} else {
printf("DeletePersistentTcpPortReservation failed: %lu\n", ret2);
}
return 0;
}
编译后必须在管理员权限 下运行,否则会收到 ERROR_ACCESS_DENIED。
使用注意点
这个 API 本质是操作注册表:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\ReservedPorts
因此:
- 必须管理员权限
- 端口范围不允许交叉
- 预留后,其他程序 Bind 那段端口会失败(WSAEACCES)
CreatePersistentUdpPortReservation:UDP 持久端口预留
cpp
IPHLPAPI_DLL_LINKAGE
ULONG
WINAPI
CreatePersistentUdpPortReservation(
_In_ USHORT StartPort,
_In_ USHORT NumberOfPorts,
_Out_ PULONG64 Token
);
CreatePersistentUdpPortReservation 用于在 Windows 系统中创建 UDP 端口的持久预留(Persistent Port Reservation) 。
被预留的端口段会从系统的动态端口池中剔除,并且在系统重启后依然有效,从而保证某些关键 UDP 服务始终拥有稳定、可用的端口资源。
该接口常用于对端口稳定性要求较高的场景,如通信中间件、VPN、专用协议栈、工业控制或长期运行的后台服务。
参数说明
StartPort
要预留的起始 UDP 端口号(主机字节序)。端口必须位于合法范围内(1--65535)。
NumberOfPorts
连续预留的端口数量。
例如 StartPort = 6000,NumberOfPorts = 10,则预留 6000--6009。
Token
输出参数,用于接收系统生成的预留标识(Token)。
该 Token 在释放端口预留时必须使用,对应的释放接口为:
c
DeletePersistentUdpPortReservation()
返回值与常见错误
返回值类型为 ULONG,与 Win32 错误码保持一致。
常见返回值如下:
| 返回值 | 含义 | 说明 |
|---|---|---|
| NO_ERROR (0) | 成功 | UDP 端口预留创建成功 |
| ERROR_ACCESS_DENIED (5) | 权限不足 | 必须以管理员权限运行 |
| ERROR_INVALID_PARAMETER (87) | 参数非法 | 端口范围错误或数量为 0 |
| ERROR_ALREADY_EXISTS (183) | 已存在 | 端口段已被预留 |
| ERROR_SHARING_VIOLATION (32) | 冲突 | 端口正在被其他服务占用 |
| ERROR_NOT_ENOUGH_MEMORY (8) | 资源不足 | 极少发生 |
如果返回非 0,说明系统拒绝了该预留请求。
简易示例代码(UDP)
c
#include <windows.h>
#include <stdio.h>
#include <iphlpapi.h>
#pragma comment(lib, "iphlpapi.lib")
int main()
{
USHORT startPort = 7000;
USHORT count = 4;
ULONG64 token = 0;
ULONG ret = CreatePersistentUdpPortReservation(
startPort,
count,
&token
);
if (ret != NO_ERROR) {
printf("CreatePersistentUdpPortReservation failed: %lu\n", ret);
return 1;
}
printf("UDP port reservation created.\n");
printf("StartPort=%u Count=%u Token=%llu\n",
startPort, count, token);
// 用完后可调用:
// DeletePersistentUdpPortReservation(startPort, count, token);
return 0;
}
程序必须在 管理员权限 下执行,否则会返回 ERROR_ACCESS_DENIED。
行为特性与注意事项
-
UDP 端口预留与 TCP 完全独立,互不影响。
-
预留后的端口:
- 不会被系统动态分配
- 普通应用 Bind 时会失败
-
预留信息持久存储于系统配置中,重启不会丢失。
-
建议在程序卸载或服务停止时显式调用删除接口,避免"僵尸预留"。
DeletePersistentTcpPortReservation:删除 TCP 持久端口预留
cpp
IPHLPAPI_DLL_LINKAGE
ULONG
WINAPI
DeletePersistentTcpPortReservation(
_In_ USHORT StartPort,
_In_ USHORT NumberOfPorts
);
DeletePersistentTcpPortReservation 用于删除已创建的 TCP 持久端口预留 。
一旦成功调用,对应端口段会从系统的"预留端口列表"中移除,随后这些端口将重新回到系统可分配状态,可被其他进程正常使用。
参数说明
StartPort
需要删除预留的起始 TCP 端口号,必须与创建预留时的起始端口完全一致。
NumberOfPorts
需要删除的连续端口数量,必须与创建时的端口数量一致。
端口范围不支持"部分删除",只能整段释放。
返回值说明
返回值类型为 ULONG,语义与 Win32 错误码一致。
常见返回值如下:
| 返回值 | 含义 | 说明 |
|---|---|---|
| NO_ERROR (0) | 成功 | 端口预留已删除 |
| ERROR_ACCESS_DENIED (5) | 权限不足 | 必须以管理员权限运行 |
| ERROR_NOT_FOUND (1168) | 未找到 | 指定端口段不存在预留 |
| ERROR_INVALID_PARAMETER (87) | 参数非法 | 端口号或数量错误 |
| ERROR_SHARING_VIOLATION (32) | 正在使用 | 某些服务仍在使用该端口 |
如果返回 ERROR_NOT_FOUND,通常意味着该端口段从未被预留,或已经被删除。
简易示例代码
c
#include <windows.h>
#include <stdio.h>
#include <iphlpapi.h>
#pragma comment(lib, "iphlpapi.lib")
int main()
{
USHORT startPort = 5000;
USHORT count = 5;
ULONG ret = DeletePersistentTcpPortReservation(startPort, count);
if (ret != NO_ERROR) {
printf("DeletePersistentTcpPortReservation failed: %lu\n", ret);
return 1;
}
printf("TCP port reservation removed.\n");
return 0;
}
示例中假定 5000--5004 端口之前已经被成功预留。
使用注意事项
-
删除操作同样需要 管理员权限。
-
端口段必须与创建时完全一致,否则系统会认为"找不到预留"。
-
删除后,端口立即恢复为系统可用状态,但:
- 已经 Bind 的 Socket 不会被强制关闭
- 新建 Socket 可以重新使用这些端口
实战建议
在实际工程中,推荐遵循以下原则:
- 创建与删除成对出现:服务启动 → 创建预留,服务卸载或停止 → 删除预留。
- 避免在调试阶段频繁创建但忘记删除,否则会造成系统长期端口占用。
- 对于运维或调试,可配合
netsh int ipv4 show excludedportrange protocol=tcp查看系统当前的端口预留状态。
DeletePersistentUdpPortReservation:删除 UDP 持久端口预留
cpp
IPHLPAPI_DLL_LINKAGE
ULONG
WINAPI
DeletePersistentUdpPortReservation(
_In_ USHORT StartPort,
_In_ USHORT NumberOfPorts
);
DeletePersistentUdpPortReservation 用于删除已创建的 UDP 持久端口预留 。
当该函数成功执行后,对应的 UDP 端口范围会从系统的预留列表中移除,这些端口将重新回到可被系统或应用程序使用的状态。
参数说明
StartPort
要删除预留的起始 UDP 端口号,必须与创建预留时使用的起始端口完全一致。
NumberOfPorts
要删除的连续端口数量,也必须与创建时的数量一致。
该接口不支持部分删除,只能整段释放。
返回值说明
返回值类型为 ULONG,采用标准 Win32 错误码语义。
常见返回值如下:
| 返回值 | 含义 | 说明 |
|---|---|---|
| NO_ERROR (0) | 成功 | UDP 端口预留已删除 |
| ERROR_ACCESS_DENIED (5) | 权限不足 | 需要管理员权限 |
| ERROR_NOT_FOUND (1168) | 未找到 | 指定端口段未被预留 |
| ERROR_INVALID_PARAMETER (87) | 参数非法 | 端口范围或数量错误 |
| ERROR_SHARING_VIOLATION (32) | 正在使用 | 某些组件仍占用该端口 |
当返回 ERROR_NOT_FOUND 时,通常表示该端口段不存在于系统的 UDP 预留列表中,或者已经被删除。
简易示例代码
c
#include <windows.h>
#include <stdio.h>
#include <iphlpapi.h>
#pragma comment(lib, "iphlpapi.lib")
int main()
{
USHORT startPort = 7000;
USHORT count = 4;
ULONG ret = DeletePersistentUdpPortReservation(startPort, count);
if (ret != NO_ERROR) {
printf("DeletePersistentUdpPortReservation failed: %lu\n", ret);
return 1;
}
printf("UDP port reservation removed.\n");
return 0;
}
示例假设 7000--7003 端口此前已经通过
CreatePersistentUdpPortReservation 成功创建预留。
使用注意事项
-
删除操作同样需要 管理员权限。
-
必须确保删除的端口范围与创建时完全一致,否则系统无法匹配对应的预留记录。
-
删除后:
- 已经存在的 Socket 不会被系统强制关闭
- 新建 Socket 可立即使用这些端口
工程实践建议
在工程实践中,UDP 端口预留通常用于长期服务或系统组件,建议:
- 将端口预留与服务生命周期绑定
- 在服务卸载或异常退出时显式清理预留
- 调试或运维阶段,可通过
netsh int ipv4 show excludedportrange protocol=udp
查看当前系统的 UDP 端口预留情况
LookupPersistentTcpPortReservation:查询 TCP 持久端口预留
cpp
IPHLPAPI_DLL_LINKAGE
ULONG
WINAPI
LookupPersistentTcpPortReservation(
_In_ USHORT StartPort,
_In_ USHORT NumberOfPorts,
_Out_ PULONG64 Token
);
LookupPersistentTcpPortReservation 用于查询指定 TCP 端口范围是否已经存在持久端口预留 ,并在存在时返回对应的预留 Token。
它不会创建或修改系统状态,仅用于"查证事实",非常适合在服务启动或安装阶段做前置校验。
参数说明
StartPort
要查询的起始 TCP 端口号,必须与预留创建时的起始端口一致。
NumberOfPorts
要查询的连续端口数量,也必须与创建预留时的数量一致。
查询粒度是"整段",不支持部分匹配。
Token
输出参数。
如果指定端口段存在预留,系统会返回对应的 Token;如果不存在,则该值不保证有效。
返回值说明
返回值类型为 ULONG,遵循 Win32 错误码语义。
常见返回值如下:
| 返回值 | 含义 | 说明 |
|---|---|---|
| NO_ERROR (0) | 存在预留 | Token 返回成功 |
| ERROR_NOT_FOUND (1168) | 不存在 | 该端口段未被预留 |
| ERROR_INVALID_PARAMETER (87) | 参数非法 | 端口范围或指针错误 |
| ERROR_ACCESS_DENIED (5) | 权限不足 | 通常仍需管理员权限 |
当返回 ERROR_NOT_FOUND 时,表示该端口范围当前没有 TCP 持久端口预留。
简易示例代码
c
#include <windows.h>
#include <stdio.h>
#include <iphlpapi.h>
#pragma comment(lib, "iphlpapi.lib")
int main()
{
USHORT startPort = 5000;
USHORT count = 5;
ULONG64 token = 0;
ULONG ret = LookupPersistentTcpPortReservation(
startPort,
count,
&token
);
if (ret == NO_ERROR) {
printf("TCP port reservation exists.\n");
printf("Start=%u Count=%u Token=%llu\n",
startPort, count, token);
} else if (ret == ERROR_NOT_FOUND) {
printf("TCP port reservation not found.\n");
} else {
printf("LookupPersistentTcpPortReservation failed: %lu\n", ret);
}
return 0;
}
典型使用场景
- 服务启动前检查:避免重复创建端口预留。
- 升级或重装程序:判断历史预留是否仍然存在。
- 运维工具:配合 Delete 接口,实现"只删存在的预留"。
通常的安全流程是:
Lookup → 若不存在则 Create → 若存在则复用或校验
注意事项
- 查询必须使用与创建时完全一致的端口范围,否则系统会认为"不存在"。
- 返回的 Token 可用于逻辑校验或日志记录,但 删除预留不需要 Token(TCP 删除接口仅依赖端口范围)。
- 查询操作不会影响系统端口分配行为。
LookupPersistentUdpPortReservation:查询一段 UDP 端口区间是否已经被系统做了持久端口保留
cpp
IPHLPAPI_DLL_LINKAGE
ULONG
WINAPI
LookupPersistentUdpPortReservation(
_In_ USHORT StartPort,
_In_ USHORT NumberOfPorts,
_Out_ PULONG64 Token
);
LookupPersistentUdpPortReservation 用来查询一段 UDP 端口区间是否已经被系统做了"持久端口保留" ,如果存在,就返回对应的 Reservation Token。
它不会创建、不修改任何东西,只是查。
参数逐一解释(精确语义)
StartPort
- 起始 UDP 端口号
- 合法范围:
1 ~ 65535 - 表示你要查询的第一个端口
NumberOfPorts
-
连续端口数量
-
查询区间是:
[StartPort, StartPort + NumberOfPorts - 1]
-
不能为 0
-
StartPort + NumberOfPorts - 1不能超过 65535
Token(输出)
-
如果查询成功:
- 返回该端口保留区间的 Reservation Token
-
如果失败:
- 内容未定义,不要使用
这个 Token 的用途是:
- 作为"标识"证明这个端口区间属于同一个 reservation
- 不是权限凭证
- 删除端口保留 不需要 Token
返回值(最关键的部分)
返回值类型是 ULONG,遵循 Win32 Error Code 语义:
✅ 成功
c
NO_ERROR (0)
含义:
- 这段 UDP 端口区间 已经存在持久保留
Token有效
❌ 常见失败返回值(实际工程会遇到的)
ERROR_NOT_FOUND (1168)
最常见。
含义:
- 这段端口区间 没有任何持久 UDP 端口保留
- 不是错误状态,只是"查无此项"
工程含义:
👉 可以安全地
CreatePersistentUdpPortReservation
ERROR_ACCESS_DENIED (5)
含义:
- 当前进程 没有管理员权限
这个 API 必须以管理员身份运行
ERROR_INVALID_PARAMETER (87)
常见触发原因:
StartPort == 0NumberOfPorts == 0- 端口范围越界
Token == NULL
最小可用示例(只做查询)
cpp
#include <windows.h>
#include <iphlpapi.h>
#include <stdio.h>
#pragma comment(lib, "iphlpapi.lib")
void LookupUdpReservation()
{
USHORT startPort = 50000;
USHORT portCount = 10;
ULONG64 token = 0;
ULONG ret = LookupPersistentUdpPortReservation(
startPort,
portCount,
&token
);
if (ret == NO_ERROR)
{
printf("UDP port reserved\n");
printf("Token = %llu\n", token);
}
else if (ret == ERROR_NOT_FOUND)
{
printf("UDP port NOT reserved\n");
}
else
{
printf("Lookup failed, error = %lu\n", ret);
}
}
工程层面的"正确理解方式"
-
查的是"系统级声明"
- 与当前是否有 socket bind 无关
-
查不到 ≠ 端口一定可用
- 只是"没有被系统保留"
-
查到 ≠ 端口正在被占用
- 只是"被预留给特定服务"
一句话总结:
LookupPersistentUdpPortReservation
回答的问题是:
"系统有没有声明过:这段 UDP 端口以后要留给某个东西用?"