From 5609d5794734362708d0926c657ecb494226cf05 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 21 Oct 2026 20:11:04 +0200 Subject: [PATCH] Fix NTP offset and bound asynchronous polling cycles --- src/ntp_time.c | 181 ++++++++++++++++++++++++++++--------------------- src/ntp_time.h | 45 ++++++++---- 2 files changed, 133 insertions(+), 93 deletions(-) diff --git a/src/ntp_time.c b/src/ntp_time.c index a5319109..a181034e 100644 --- a/src/ntp_time.c +++ b/src/ntp_time.c @@ -9,6 +9,7 @@ #include "../lib/async_dns.h" #include #include +#include #define NTP_DELTA 2208988800ULL // seconds from 1900 to 1970 #define NTP_PORT 123 @@ -38,13 +39,13 @@ _Static_assert(sizeof(struct ntp_packet) == 48, "NTP packet must be 48 bytes"); struct ntp_sync { struct UTUN_INSTANCE* inst; int server_idx; /* следующий сервер для попытки */ + int servers_left; /* остаток цикла, общий для синхронных и асинхронных ошибок */ int query_idx; /* сервер, который сейчас опрашиваем (-1 для test_addr) */ - int resolving_idx; /* сервер в процессе DNS-резолва */ struct adns_query* dns_q; /* активный DNS-запрос (NULL когда нет) */ socket_t sock; /* NTP UDP-сокет (SOCKET_INVALID когда закрыт) */ void* socket_id; /* handle uasync */ void* timer; /* handle таймаута запроса */ - uint8_t req[48]; /* NTP-запрос (xmit_ts заполняется один раз на сервер) */ + uint8_t req[48]; /* xmit_ts обновляется при каждой отправке, включая повторы */ uint64_t t1_ntp; /* originate timestamp */ int tries; /* попыток на текущем сервере */ struct sockaddr_in qaddr;/* адрес текущего сервера */ @@ -52,9 +53,9 @@ struct ntp_sync { static void ntp_time_sync_cb(void* arg); static void ntp_try_next(struct ntp_sync* ctx); -static void ntp_start_query(struct ntp_sync* ctx, int idx, const struct sockaddr_in* addr); -static void ntp_send_request(struct ntp_sync* ctx); -static void ntp_query_fail(struct ntp_sync* ctx); +static int ntp_start_query(struct ntp_sync* ctx, const struct sockaddr_in* addr); +static int ntp_send_request(struct ntp_sync* ctx); +static void ntp_close_query(struct ntp_sync* ctx); static void ntp_finish_cycle(struct ntp_sync* ctx, int synced); static void ntp_resolve_done_cb(const struct adns_result* res, void* arg); static void ntp_query_read_cb(socket_t sock, void* arg); @@ -115,7 +116,7 @@ static int ntp_parse_reply(const uint8_t* buf, size_t len, uint64_t t1_ntp, int64_t t3_us = ntp64_to_us(be64toh(reply->xmit_ts)); int64_t t4_us = ntp64_to_us(timeval_to_ntp(&t4_tv)); - int64_t offset_us = ((t2_us - t1_us) + (t3_us - t4_us)) / 2; + int64_t offset_us = ((t1_us - t2_us) + (t4_us - t3_us)) / 2; /* local - reference */ int64_t rtt_us = (t4_us - t1_us) - (t3_us - t2_us); *offset_us_out = offset_us; @@ -130,21 +131,47 @@ static uint16_t ntp_query_port(const struct NTP_TIME* ntp) { return ntp->test_port ? ntp->test_port : NTP_PORT; } +static int ntp_schedule_sync(struct UTUN_INSTANCE* inst, int delay_sec) { + if (delay_sec < 0 || delay_sec > INT_MAX / 10000) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: timer delay out of range: %ds", delay_sec); + return -1; + } + inst->ntp.timer = uasync_set_timeout(inst->ua, delay_sec * 10000, inst, ntp_time_sync_cb, "ntp_sync"); + if (!inst->ntp.timer) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: failed to schedule sync in %ds; automatic polling stopped", delay_sec); + return -1; + } + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "NTP: next cycle in %ds", delay_sec); + return 0; +} + static void ntp_try_next(struct ntp_sync* ctx) { struct NTP_TIME* ntp = &ctx->inst->ntp; + ntp_close_query(ctx); - for (int i = 0; i < ntp->server_count; i++) { + while (ctx->servers_left > 0) { + ctx->servers_left--; + if (ntp->test_addr.sin_port != 0) { + ctx->query_idx = -1; + if (ntp_start_query(ctx, &ntp->test_addr) == 0) return; + ntp_close_query(ctx); + continue; + } int idx = ctx->server_idx; + ctx->query_idx = idx; ctx->server_idx = (idx + 1) % ntp->server_count; const char* name = ntp->servers[idx]; + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "NTP: polling server %d/%d '%s', remaining=%d", + idx + 1, ntp->server_count, name, ctx->servers_left); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(ntp_query_port(ntp)); if (inet_pton(AF_INET, name, &addr.sin_addr) == 1) { - ntp_start_query(ctx, idx, &addr); - return; + if (ntp_start_query(ctx, &addr) == 0) return; + ntp_close_query(ctx); + continue; } struct adns_opts dopts, *popts = NULL; @@ -154,7 +181,6 @@ static void ntp_try_next(struct ntp_sync* ctx) { popts = &dopts; } - ctx->resolving_idx = idx; ctx->dns_q = adns_resolve(ctx->inst->ua, name, popts, ntp_resolve_done_cb, ctx); if (ctx->dns_q) return; DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: resolve '%s' failed immediately", name); @@ -168,63 +194,66 @@ static void ntp_resolve_done_cb(const struct adns_result* res, void* arg) { struct ntp_sync* ctx = (struct ntp_sync*)arg; ctx->dns_q = NULL; struct NTP_TIME* ntp = &ctx->inst->ntp; - int idx = ctx->resolving_idx; + int idx = ctx->query_idx; if (res->status == ADNS_OK && res->count > 0) { struct sockaddr_in addr = res->addrs[0]; addr.sin_port = htons(ntp_query_port(ntp)); - ntp_start_query(ctx, idx, &addr); + if (ntp_start_query(ctx, &addr) == 0) return; } else { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: resolve '%s' failed (status=%d)", ntp->servers[idx], res->status); - ntp_try_next(ctx); } + ntp_try_next(ctx); } -static void ntp_start_query(struct ntp_sync* ctx, int idx, const struct sockaddr_in* addr) { - ctx->query_idx = idx; +static int ntp_start_query(struct ntp_sync* ctx, const struct sockaddr_in* addr) { ctx->qaddr = *addr; ctx->tries = 0; - ntp_send_request(ctx); + return ntp_send_request(ctx); } -static void ntp_send_request(struct ntp_sync* ctx) { +static int ntp_send_request(struct ntp_sync* ctx) { struct UTUN_INSTANCE* inst = ctx->inst; if (ctx->sock == SOCKET_INVALID) { ctx->sock = socket_create_udp(AF_INET); if (ctx->sock == SOCKET_INVALID) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: socket() failed: %s", socket_strerror(socket_get_error())); - ntp_query_fail(ctx); - return; + return -1; + } + if (socket_set_nonblocking(ctx->sock) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: nonblocking mode failed: %s", socket_strerror(socket_get_error())); + return -1; } - socket_set_nonblocking(ctx->sock); } - if (ctx->tries == 0) ntp_build_request(ctx->req, &ctx->t1_ntp); + ntp_build_request(ctx->req, &ctx->t1_ntp); + ctx->tries++; ssize_t sent = socket_sendto(ctx->sock, ctx->req, sizeof(ctx->req), (const struct sockaddr*)&ctx->qaddr, sizeof(ctx->qaddr)); - if (sent < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: sendto failed: %s", socket_strerror(socket_get_error())); - ntp_query_fail(ctx); - return; + if (sent != (ssize_t)sizeof(ctx->req)) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: sendto failed/short: sent=%lld error=%s", + (long long)sent, sent < 0 ? socket_strerror(socket_get_error()) : "short datagram"); + return -1; } if (!ctx->socket_id) { ctx->socket_id = uasync_add_socket_t(inst->ua, ctx->sock, ntp_query_read_cb, NULL, NULL, "ntp_query", ctx); if (!ctx->socket_id) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: uasync_add_socket_t failed"); - ntp_query_fail(ctx); - return; + return -1; } } ctx->timer = uasync_set_timeout(inst->ua, NTP_TIMEOUT_SEC * 10000, ctx, ntp_query_timeout_cb, "ntp_q"); if (!ctx->timer) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: uasync_set_timeout failed"); - ntp_query_fail(ctx); - return; + return -1; } + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "NTP: sent request server=%d attempt=%d/%d timestamp=%llu", + ctx->query_idx, ctx->tries, NTP_MAX_TRIES, (unsigned long long)ctx->t1_ntp); + return 0; } static void ntp_query_read_cb(socket_t sock, void* arg) { @@ -238,8 +267,11 @@ static void ntp_query_read_cb(socket_t sock, void* arg) { int64_t offset_us, rtt_us; int stratum; - if (ntp_parse_reply((const uint8_t*)&reply, (size_t)n, ctx->t1_ntp, &offset_us, &rtt_us, &stratum) != 0) - return; /* невалидный ответ — ждём (timeout перезапросит) */ + if (ntp_parse_reply((const uint8_t*)&reply, (size_t)n, ctx->t1_ntp, &offset_us, &rtt_us, &stratum) != 0) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: ignoring invalid/stale reply: bytes=%lld server=%d", + (long long)n, ctx->query_idx); + return; + } struct NTP_TIME* ntp = &ctx->inst->ntp; ntp->offset_us = offset_us; @@ -258,33 +290,25 @@ static void ntp_query_read_cb(socket_t sock, void* arg) { static void ntp_query_timeout_cb(void* arg) { struct ntp_sync* ctx = (struct ntp_sync*)arg; ctx->timer = NULL; - ctx->tries++; + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: response timeout server=%d attempt=%d/%d", + ctx->query_idx, ctx->tries, NTP_MAX_TRIES); if (ctx->tries < NTP_MAX_TRIES) { - ntp_send_request(ctx); - return; + if (ntp_send_request(ctx) == 0) return; } - ntp_query_fail(ctx); + ntp_try_next(ctx); } -static void ntp_query_fail(struct ntp_sync* ctx) { - struct NTP_TIME* ntp = &ctx->inst->ntp; - const char* name = ctx->query_idx >= 0 ? ntp->servers[ctx->query_idx] : "test-server"; - +static void ntp_close_query(struct ntp_sync* ctx) { if (ctx->timer) { uasync_cancel_timeout(ctx->inst->ua, ctx->timer); ctx->timer = NULL; } if (ctx->socket_id) { uasync_remove_socket_t(ctx->inst->ua, ctx->sock); ctx->socket_id = NULL; } if (ctx->sock != SOCKET_INVALID) { socket_close_wrapper(ctx->sock); ctx->sock = SOCKET_INVALID; } - - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: no response from %s after %d tries", name, ctx->tries); - ntp_try_next(ctx); } static void ntp_finish_cycle(struct ntp_sync* ctx, int synced) { struct UTUN_INSTANCE* inst = ctx->inst; struct NTP_TIME* ntp = &inst->ntp; - if (ctx->timer) { uasync_cancel_timeout(inst->ua, ctx->timer); ctx->timer = NULL; } - if (ctx->socket_id) { uasync_remove_socket_t(inst->ua, ctx->sock); ctx->socket_id = NULL; } - if (ctx->sock != SOCKET_INVALID) { socket_close_wrapper(ctx->sock); ctx->sock = SOCKET_INVALID; } + ntp_close_query(ctx); if (ctx->dns_q) { adns_cancel(ctx->dns_q); ctx->dns_q = NULL; } ntp->sync = NULL; @@ -304,8 +328,7 @@ static void ntp_finish_cycle(struct ntp_sync* ctx, int synced) { } } - ntp->timer = uasync_set_timeout(inst->ua, ntp->resync_interval_sec * 10000, - inst, ntp_time_sync_cb, "ntp_sync"); + ntp_schedule_sync(inst, ntp->resync_interval_sec); } static void ntp_time_sync_cb(void* arg) { @@ -315,40 +338,39 @@ static void ntp_time_sync_cb(void* arg) { ntp->timer = NULL; if (!ntp->enabled || ntp->server_count == 0) { - ntp->timer = uasync_set_timeout(instance->ua, ntp->resync_interval_sec * 10000, - instance, ntp_time_sync_cb, "ntp_sync"); return; } if (ntp->sync) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: sync already in progress, skipping"); - ntp->timer = uasync_set_timeout(instance->ua, ntp->resync_interval_sec * 10000, - instance, ntp_time_sync_cb, "ntp_sync"); return; } struct ntp_sync* ctx = u_calloc(1, sizeof(*ctx)); if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: calloc sync ctx failed"); - ntp->timer = uasync_set_timeout(instance->ua, ntp->resync_interval_sec * 10000, - instance, ntp_time_sync_cb, "ntp_sync"); + ntp_schedule_sync(instance, ntp->resync_interval_sec); return; } ctx->inst = instance; ctx->sock = SOCKET_INVALID; ntp->sync = ctx; - if (ntp->test_addr.sin_port != 0) { - ntp_start_query(ctx, -1, &ntp->test_addr); - } else { - ctx->server_idx = ntp->server_current; - ntp_try_next(ctx); - } + ctx->server_idx = ntp->server_current; + ctx->servers_left = ntp->test_addr.sin_port ? 1 : ntp->server_count; + ntp_try_next(ctx); } int ntp_time_init(struct UTUN_INSTANCE* instance) { - if (!instance) return -1; + if (!instance || !instance->config || !instance->ua) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: init requires instance, config and uasync"); + return -1; + } struct NTP_TIME* ntp = &instance->ntp; + if (ntp->servers || ntp->timer || ntp->sync) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: destroy previous state before init"); + return -1; + } struct global_config* g = &instance->config->global; ntp->enabled = g->ntp_enabled; @@ -366,24 +388,29 @@ int ntp_time_init(struct UTUN_INSTANCE* instance) { memset(&ntp->test_addr, 0, sizeof(ntp->test_addr)); memset(&ntp->test_dns, 0, sizeof(ntp->test_dns)); + if (g->ntp_server_count < 0 || (ntp->enabled && ntp->server_count > 0 && + (ntp->resync_interval_sec <= 0 || ntp->resync_interval_sec > INT_MAX / 10000))) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: invalid config: servers=%d interval=%ds", + ntp->server_count, ntp->resync_interval_sec); + goto fail; + } if (g->ntp_server_count > 0) { - ntp->servers = u_malloc(g->ntp_server_count * sizeof(char*)); + ntp->servers = u_calloc(g->ntp_server_count, sizeof(char*)); if (!ntp->servers) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "NTP: failed to allocate servers array"); - ntp->server_count = 0; - return -1; + goto fail; } struct CFG_NTP_SERVER *ns = g->ntp_servers; int i = 0; - while (ns && i < g->ntp_server_count) { + while (i < g->ntp_server_count) { + if (!ns || !ns->name[0]) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: missing/empty server at index %d", i); + goto fail; + } ntp->servers[i] = u_strdup(ns->name); if (!ntp->servers[i]) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "NTP: failed to duplicate server name"); - while (--i >= 0) u_free(ntp->servers[i]); - u_free(ntp->servers); - ntp->servers = NULL; - ntp->server_count = 0; - return -1; + goto fail; } i++; ns = ns->next; @@ -398,13 +425,11 @@ int ntp_time_init(struct UTUN_INSTANCE* instance) { DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: initialized, %d servers, interval=%ds, first sync immediately", ntp->server_count, ntp->resync_interval_sec); - ntp->timer = uasync_set_timeout(instance->ua, NTP_FIRST_SYNC_DELAY_SEC * 10000, - instance, ntp_time_sync_cb, "ntp_sync"); - if (!ntp->timer) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: failed to start sync timer"); - return -1; - } + if (ntp_schedule_sync(instance, NTP_FIRST_SYNC_DELAY_SEC) != 0) goto fail; return 0; +fail: + ntp_time_destroy(instance); + return -1; } void ntp_time_destroy(struct UTUN_INSTANCE* instance) { @@ -413,9 +438,7 @@ void ntp_time_destroy(struct UTUN_INSTANCE* instance) { if (ntp->sync && instance->ua) { struct ntp_sync* ctx = (struct ntp_sync*)ntp->sync; - if (ctx->timer) { uasync_cancel_timeout(instance->ua, ctx->timer); ctx->timer = NULL; } - if (ctx->socket_id) { uasync_remove_socket_t(instance->ua, ctx->sock); ctx->socket_id = NULL; } - if (ctx->sock != SOCKET_INVALID) socket_close_wrapper(ctx->sock); + ntp_close_query(ctx); if (ctx->dns_q) adns_cancel(ctx->dns_q); u_free(ctx); ntp->sync = NULL; @@ -431,6 +454,8 @@ void ntp_time_destroy(struct UTUN_INSTANCE* instance) { ntp->servers = NULL; } ntp->enabled = 0; + ntp->server_count = 0; + ntp->server_current = 0; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: destroyed (synced=%d, offset=%lldus)", ntp->synced, (long long)ntp->offset_us); } diff --git a/src/ntp_time.h b/src/ntp_time.h index 5237aeb4..0adb95cf 100644 --- a/src/ntp_time.h +++ b/src/ntp_time.h @@ -1,8 +1,11 @@ /* ntp_time — асинхронные UDP NTP-запросы с DNS и периодическим повтором в uasync-потоке ядра. * Не меняет часы ОС: хранит local_time - ntp_time в instance->ntp.offset_us. * get_us/get_seconds возвращают скорректированное wall-clock время; до синхронизации — локальное. - * init читает config и планирует первый цикл, destroy отменяет DNS/сокет/таймеры. - * synced означает наличие коррекции (в том числе полученной от пира), reachable — результат интернет-NTP. */ + * В uasync-потоке вызвать init после создания config/ua, destroy — до освобождения ua, вне callbacks модуля. + * Один цикл обходит серверы до первого успеха; на сервер — до трёх UDP-запросов с таймаутом 2 с. + * Между завершением цикла и следующим запуском — resync_interval_sec. DNS выполняется асинхронно (IPv4). + * enabled управляет только интернет-опросом; synced учитывает также коррекцию от ntp_node_time. + * destroy отменяет DNS/сокет/таймеры, сохраняя synced, offset_us и last_sync_tb. Часы ОС не меняются. */ #ifndef NTP_TIME_H #define NTP_TIME_H @@ -20,30 +23,42 @@ struct UTUN_INSTANCE; #define NTP_FIRST_SYNC_DELAY_SEC 0 struct NTP_TIME { - int enabled; - int synced; - int reachable; // доступность интернет-NTP по циклам (1=есть интернет, 0=нет); для классификации сокетов - int64_t offset_us; // local_time - ntp_time (positive = local clock ahead) - uint64_t last_sync_tb; // monotonic time of last successful sync (0.1ms units) - void* timer; // uasync timer handle - char **servers; // dynamic array, allocated at init - int server_count; - int server_current; - int resync_interval_sec; - struct sockaddr_in test_addr; // test mode: direct address when sin_port != 0 - struct sockaddr_in test_dns; // test mode: DNS server for hostname resolution (sin_port != 0) - uint16_t test_port; // test mode: NTP query port override (0 = 123) + int enabled; // интернет-опрос разрешён конфигом; 0 не запрещает коррекцию от пиров + int synced; // есть поправка от NTP или пира; не означает её свежесть/доступность интернета + int reachable; // успех последнего завершённого NTP-цикла; до первого результата = 1 (оптимистично) + int64_t offset_us; // local_time - reference_time, мкс; положительное = локальные часы спешат + uint64_t last_sync_tb; // монотонное время последней коррекции от NTP/пира, единицы 0,1 мс; 0 до синхронизации + void* timer; // таймер следующего цикла, NULL во время опроса/после остановки/при ошибке таймера + char **servers; // принадлежащие модулю копии IPv4-адресов/имён серверов, освобождаются destroy + int server_count; // число элементов servers; после destroy = 0 + int server_current; // начальный индекс следующего цикла; после успеха — следующий за ответившим + int resync_interval_sec;// пауза после цикла, секунды; при активном опросе 1..INT_MAX/10000 + struct sockaddr_in test_addr; // sin_port != 0: единственный сервер цикла, без DNS/перехода к servers + struct sockaddr_in test_dns; // sin_port != 0: IPv4 DNS-сервер для тестов + uint16_t test_port; // порт запросов к servers в тестах: 0 = 123; не влияет на test_addr void* sync; // активный контекст синхронизации (struct ntp_sync*, opaque) }; +// Читает instance->config, копирует серверы, ставит таймер 0 (запуск при обработке uasync, не внутри init). +// instance/config/ua обязательны, состояние исходно нулевое либо после destroy; повторный активный init запрещён. +// 0 — успех (включая отключённый опрос/пустой список), -1 — ошибка с освобождением новых ресурсов. +// Ошибка повторного init сохраняет прежнее состояние. Повторная инициализация сбрасывает поправку и test_*. int ntp_time_init(struct UTUN_INSTANCE* instance); +// Отменяет опрос и освобождает серверы; поправку/reachable сохраняет, enabled сбрасывает. NULL допустим. void ntp_time_destroy(struct UTUN_INSTANCE* instance); +// После init, до первого poll задать IPv4-литерал ip и порт сервера; port=0 отключает подмену. +// Неверный ip сбрасывает test_addr; NULL ntp/ip игнорируется. Требуется непустой список серверов в конфиге. void ntp_time_set_test_addr(struct NTP_TIME* ntp, const char* ip, uint16_t port); +// После init, до poll задать IPv4-литерал ip, dns_port (0 отключает подмену DNS) и ntp_port (0 = 123). +// Неверный ip сбрасывает test_dns и test_port; NULL ntp/ip игнорируется. test_addr имеет приоритет. void ntp_time_set_test_dns(struct NTP_TIME* ntp, const char* ip, uint16_t dns_port, uint16_t ntp_port); +// Unix wall-clock в мкс: local - offset_us при synced, иначе локальное время; NULL возвращает локальное. int64_t ntp_time_get_us(struct UTUN_INSTANCE* instance); +// То же время в целых секундах: get_us()/1000000 с приведением к time_t; NULL допустим. time_t ntp_time_get_seconds(struct UTUN_INSTANCE* instance); +// Ненулевое значение, если есть поправка от NTP/пира (включая после destroy); для NULL — 0. int ntp_time_is_synced(struct UTUN_INSTANCE* instance); #ifdef __cplusplus