nim-libplum/libplum/plum.nim

296 lines
9.3 KiB
Nim
Raw Normal View History

2026-05-14 12:23:10 +04:00
# Copyright (c) 2026 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
import std/[atomics, locks, tables]
import chronos
import chronos/threadsync
import results
import ./libplum
2026-06-01 15:27:47 +04:00
# libplum declares some parameters as `const T*` in C (read-only pointer).
# Nim has no equivalent, so the generated C code drops the `const`, causing
# a type mismatch warning in GCC 15+. This pragma suppresses that warning
# only in this translation unit and is valid for both C and C++.
2026-05-19 14:15:26 +04:00
{.
emit: """
2026-05-19 11:28:20 +04:00
#ifdef __GNUC__
#pragma GCC diagnostic ignored "-Wincompatible-pointer-types"
#endif
2026-05-19 14:15:26 +04:00
"""
.}
2026-05-19 11:28:20 +04:00
2026-05-14 12:23:10 +04:00
export results
{.pragma: callback, cdecl, raises: [], gcsafe.}
2026-06-01 18:30:04 +04:00
{.push raises: [].}
2026-05-14 12:23:10 +04:00
type
PlumProtocol* = enum
TCP = PLUM_IP_PROTOCOL_TCP.int
UDP = PLUM_IP_PROTOCOL_UDP.int
2026-06-01 18:33:20 +04:00
PlumLogLevel* {.pure.} = enum
Verbose = PLUM_LOG_LEVEL_VERBOSE.int
Debug = PLUM_LOG_LEVEL_DEBUG.int
Info = PLUM_LOG_LEVEL_INFO.int
Warn = PLUM_LOG_LEVEL_WARN.int
Error = PLUM_LOG_LEVEL_ERROR.int
Fatal = PLUM_LOG_LEVEL_FATAL.int
None = PLUM_LOG_LEVEL_NONE.int
2026-05-14 12:23:10 +04:00
PlumState* = enum
2026-05-19 14:15:26 +04:00
Destroyed = PLUM_STATE_DESTROYED.int
Pending = PLUM_STATE_PENDING.int
Success = PLUM_STATE_SUCCESS.int
Failure = PLUM_STATE_FAILURE.int
2026-05-14 12:23:10 +04:00
Destroying = PLUM_STATE_DESTROYING.int
2026-05-18 15:16:27 +04:00
MappingProtocol* = enum
Unknown = PLUM_MAPPING_PROTOCOL_UNKNOWN.int
2026-05-19 14:15:26 +04:00
PCP = PLUM_MAPPING_PROTOCOL_PCP.int
NatPmp = PLUM_MAPPING_PROTOCOL_NATPMP.int
UPnP = PLUM_MAPPING_PROTOCOL_UPNP.int
Direct = PLUM_MAPPING_PROTOCOL_DIRECT.int
2026-05-18 15:16:27 +04:00
2026-05-14 12:23:10 +04:00
PlumMapping* = object
protocol*: PlumProtocol
2026-05-18 15:16:27 +04:00
mappingProtocol*: MappingProtocol
2026-05-14 12:23:10 +04:00
internalPort*: uint16
externalPort*: uint16
externalHost*: string
MappingResult* = object
id*: cint
mapping*: PlumMapping
2026-05-19 14:15:26 +04:00
PlumStateCallback* = proc(state: PlumState, mapping: PlumMapping) {.callback.}
## Invoked on mapping state changes after the initial result. Runs on
## libplum's internal C thread, not the chronos loop: only touch
## thread-safe state from it (e.g. Atomic), never chronos APIs.
2026-05-14 12:23:10 +04:00
MappingHandle = ref object
signal: ThreadSignalPtr
# Indicate that the first call of the mapping handle is
# done. In that case, resolved* variables
2026-05-14 12:23:10 +04:00
# will contain the result.
# MappingHandle can be called multiple times after that,
# but the result will be passed through the callback.
resolved: Atomic[bool]
resolvedState: PlumState
resolvedProtocol: PlumProtocol
2026-05-18 15:16:27 +04:00
resolvedMappingProtocol: MappingProtocol
resolvedInternalPort: uint16
resolvedExternalPort: uint16
resolvedExternalHost: array[PLUM_MAX_HOST_LEN, char]
2026-06-01 18:22:53 +04:00
# Refcount-like
signalReleases: Atomic[int]
2026-05-14 12:23:10 +04:00
onStateChange: PlumStateCallback
# libplum calls mappingCallback from its own C thread. Under refc, any thread
# that touches Nim objects must register with the GC first.
template foreignThreadGc(body: untyped) =
2026-05-19 14:15:26 +04:00
when declared(setupForeignThreadGc):
setupForeignThreadGc()
try:
body
finally:
when declared(tearDownForeignThreadGc):
tearDownForeignThreadGc()
2026-05-14 12:23:10 +04:00
var activeMappingsLock: Lock
var activeMappings {.guard: activeMappingsLock.}: Table[cint, MappingHandle]
initLock(activeMappingsLock)
2026-06-01 18:30:04 +04:00
proc releaseSignal(handle: MappingHandle) =
2026-06-01 18:22:53 +04:00
if handle.signalReleases.fetchAdd(1) == 1:
discard handle.signal.close()
2026-05-14 12:23:10 +04:00
# We can be confident that the pattern is GC Safe using
# a lock.
template withSafeLock(body: untyped) =
{.cast(gcsafe).}:
withLock activeMappingsLock:
body
2026-06-01 18:38:47 +04:00
proc mappingCallback(id: cint, state: plum_state_t, raw: ptr plum_mapping_t) {.cdecl.} =
2026-05-14 12:23:10 +04:00
## Called from libplum's internal C thread on SUCCESS, FAILURE, and DESTROYED.
foreignThreadGc:
let handle = cast[MappingHandle](raw[].user_ptr)
if handle.isNil:
return
let plumState = PlumState(state.int)
if plumState == Destroyed:
withSafeLock:
activeMappings.del(id)
2026-06-01 18:22:53 +04:00
handle.releaseSignal()
# Release the pin set in createMapping: the C library is done with the
# raw pointer and will never call this callback again for this mapping.
GC_unref(handle)
2026-05-14 12:23:10 +04:00
return
# Skip states other than Success and Failure
if plumState notin {Success, Failure}:
return
if not handle.resolved.exchange(true):
# If this is the first time the handle mapping
# is called, let's set the result in the handle values,
# and fire the signal.
handle.resolvedState = plumState
handle.resolvedProtocol = PlumProtocol(raw[].protocol.int)
2026-05-18 15:16:27 +04:00
handle.resolvedMappingProtocol = MappingProtocol(raw[].mapping_protocol.int)
handle.resolvedInternalPort = raw[].internal_port
handle.resolvedExternalPort = raw[].external_port
handle.resolvedExternalHost = raw[].external_host
2026-05-14 12:23:10 +04:00
discard handle.signal.fireSync()
else:
# Otherwise, just call the callback
if not handle.onStateChange.isNil:
let mapping = PlumMapping(
protocol: PlumProtocol(raw[].protocol.int),
2026-05-18 15:16:27 +04:00
mappingProtocol: MappingProtocol(raw[].mapping_protocol.int),
internalPort: raw[].internal_port,
externalPort: raw[].external_port,
2026-05-19 14:15:26 +04:00
externalHost: $cast[cstring](addr raw[].external_host),
)
2026-05-14 12:23:10 +04:00
handle.onStateChange(plumState, mapping)
2026-05-14 16:40:10 +04:00
proc init*(
2026-06-01 18:33:20 +04:00
logLevel: PlumLogLevel = PlumLogLevel.None,
discoverTimeout: int32 = 0,
mappingTimeout: int32 = 0,
recheckPeriod: int32 = 0,
2026-06-01 18:30:04 +04:00
): Result[void, string] =
2026-05-14 12:23:10 +04:00
## init MUST be called to setup internal plum thread (plum_init).
var config = plum_config_t(
2026-06-01 18:33:20 +04:00
log_level: plum_log_level_t(logLevel.int),
2026-05-14 12:23:10 +04:00
log_callback: nil,
2026-05-14 16:40:10 +04:00
dummytls_domain: nil,
discover_timeout: discoverTimeout.cint,
mapping_timeout: mappingTimeout.cint,
2026-05-19 14:15:26 +04:00
recheck_period: recheckPeriod.cint,
2026-05-14 12:23:10 +04:00
)
let res = plum_init(addr config)
if res == PLUM_ERR_SUCCESS:
ok()
else:
err("plum_init failed: " & $res)
2026-06-01 18:30:04 +04:00
proc cleanup*(): Result[void, string] =
2026-05-14 12:23:10 +04:00
## cleanup MUST be called to stop the thread and clean the setup.
let res = plum_cleanup()
if res == PLUM_ERR_SUCCESS:
ok()
else:
err("plum_cleanup failed: " & $res)
proc createMapping*(
protocol: PlumProtocol,
internalPort: uint16,
externalPort: uint16 = 0,
2026-05-14 16:40:10 +04:00
timeout: Duration = seconds(30),
2026-05-19 14:15:26 +04:00
onStateChange: PlumStateCallback = nil,
2026-05-14 12:23:10 +04:00
): Future[Result[MappingResult, string]] {.async: (raises: [CancelledError]).} =
let signal = ThreadSignalPtr.new().valueOr:
return err("plum: cannot create signal: " & $error)
2026-05-19 14:15:26 +04:00
let handle = MappingHandle(signal: signal, onStateChange: onStateChange)
2026-05-14 12:23:10 +04:00
var req = plum_mapping_t(
protocol: plum_ip_protocol_t(protocol.int),
internal_port: internalPort,
external_port: externalPort,
2026-05-19 14:15:26 +04:00
user_ptr: cast[pointer](handle),
2026-05-14 12:23:10 +04:00
)
# Pin the handle to prevent GC: the C library holds a raw pointer to
# it (user_ptr) and might use it until DESTROYED fires.
GC_ref(handle)
2026-05-14 12:23:10 +04:00
let id = plum_create_mapping(addr req, mappingCallback)
if id < 0:
GC_unref(handle)
2026-05-14 12:23:10 +04:00
discard signal.close()
return err("plum_create_mapping failed: " & $id)
withSafeLock:
activeMappings[id] = handle
var completed = false
try:
# Wait for the callback to fireSync
2026-05-14 16:40:10 +04:00
completed = await withTimeout(signal.wait(), timeout)
2026-05-14 12:23:10 +04:00
except CancelledError:
2026-05-14 16:40:10 +04:00
# CancelledError skips the lines below and propagates after finally.
2026-05-14 12:23:10 +04:00
raise
finally:
if not completed:
2026-06-01 18:22:53 +04:00
# Timeout or cancellation: a late callback may still fireSync, so the
# mapping is torn down and the close is decided by releaseSignal.
2026-05-14 12:23:10 +04:00
discard plum_destroy_mapping(id)
2026-06-01 18:22:53 +04:00
handle.releaseSignal()
2026-05-14 16:40:10 +04:00
2026-06-01 18:27:52 +04:00
# Timeout path: the signal never fired within the deadline.
2026-05-14 16:40:10 +04:00
if not completed:
2026-05-14 12:23:10 +04:00
return err("plum: mapping " & $id & " timed out")
2026-06-01 18:22:53 +04:00
let resolvedState = handle.resolvedState
let resolvedMapping = PlumMapping(
protocol: handle.resolvedProtocol,
mappingProtocol: handle.resolvedMappingProtocol,
internalPort: handle.resolvedInternalPort,
externalPort: handle.resolvedExternalPort,
externalHost: $cast[cstring](unsafeAddr handle.resolvedExternalHost),
)
if resolvedState == Success:
return ok(MappingResult(id: id, mapping: resolvedMapping))
2026-05-14 12:23:10 +04:00
else:
discard plum_destroy_mapping(id)
return err("plum: mapping " & $id & " failed")
2026-06-01 18:30:04 +04:00
proc destroyMapping*(id: cint) =
2026-05-14 12:23:10 +04:00
## Must be called exactly once after a successful createMapping.
## Safe to call again or on an unknown id
withSafeLock:
if id notin activeMappings:
return
2026-05-14 12:23:10 +04:00
discard plum_destroy_mapping(id)
2026-06-01 18:30:04 +04:00
proc hasMapping*(id: cint): bool =
## Returns true if the mapping exists and is not being destroyed.
var st: plum_state_t
if plum_query_mapping(id, addr st, nil) == PLUM_ERR_SUCCESS:
PlumState(st.int) notin {Destroying, Destroyed}
else:
false
2026-05-14 16:40:10 +04:00
2026-06-01 18:30:04 +04:00
proc activeMappingCount*(): int =
2026-06-01 17:22:10 +04:00
## Number of mappings the wrapper still tracks. Drops to 0 once every
## mapping has fired DESTROYED. Mainly useful to detect handle leaks.
withSafeLock:
result = activeMappings.len
2026-06-01 18:30:04 +04:00
proc getLocalAddress*(): Result[string, string] =
2026-05-14 12:23:10 +04:00
var buf = newString(PLUM_MAX_ADDRESS_LEN)
let res = plum_get_local_address(buf.cstring, buf.len.csize_t)
if res >= 0:
2026-05-14 16:40:10 +04:00
buf.setLen(min(res.int, PLUM_MAX_ADDRESS_LEN))
2026-05-14 12:23:10 +04:00
ok(buf)
else:
err("plum_get_local_address failed: " & $res)
2026-06-01 18:30:04 +04:00
{.pop.}