import ctypes
import platform
import sys
import time
from ctypes import c_uint8, c_uint64, c_void_p, POINTER
from pathlib import Path
def _find_lib():
bridge_dir = Path(__file__).resolve().parent.parent / "effects_bridge" / "target" / "debug"
if platform.system() == "Darwin":
return bridge_dir / "libeffects_bridge.dylib"
return bridge_dir / "libeffects_bridge.so"
lib = ctypes.CDLL(str(_find_lib()))
class EffectPollResult(ctypes.Structure):
_fields_ = [
("status", c_uint8),
("_padding", c_uint8 * 7),
("value_a", c_uint64),
("value_b", c_uint64),
]
EFFECT_NONE = 0
EFFECT_GET_TIMESTAMP = 1
EFFECT_LOG = 2
lib.effects_counter_new.argtypes = [c_uint64]
lib.effects_counter_new.restype = c_void_p
lib.effects_counter_free.argtypes = [c_void_p]
lib.effects_counter_free.restype = None
lib.counter_value_at_now.argtypes = [c_void_p]
lib.counter_value_at_now.restype = c_void_p
lib.counter_add_with_log.argtypes = [c_void_p, c_uint64]
lib.counter_add_with_log.restype = c_void_p
lib.effects_future_poll.argtypes = [c_void_p]
lib.effects_future_poll.restype = EffectPollResult
lib.pending_effect.argtypes = [c_void_p]
lib.pending_effect.restype = c_uint8
lib.pending_log_message.argtypes = [c_void_p]
lib.pending_log_message.restype = POINTER(c_uint8)
lib.pending_log_message_len.argtypes = [c_void_p]
lib.pending_log_message_len.restype = c_uint64
lib.fulfill_timestamp.argtypes = [c_void_p, c_uint64]
lib.fulfill_timestamp.restype = None
lib.fulfill_log.argtypes = [c_void_p]
lib.fulfill_log.restype = None
lib.effects_future_free.argtypes = [c_void_p]
lib.effects_future_free.restype = None
def poll_with_effects(future_handle):
while True:
result = lib.effects_future_poll(future_handle)
if result.status != 0:
return result.value_a, result.value_b
effect = lib.pending_effect(future_handle)
if effect == EFFECT_GET_TIMESTAMP:
now_ms = int(time.time() * 1000)
lib.fulfill_timestamp(future_handle, now_ms)
elif effect == EFFECT_LOG:
ptr = lib.pending_log_message(future_handle)
length = lib.pending_log_message_len(future_handle)
msg = ctypes.string_at(ptr, length).decode("utf-8")
print(f" [LOG] {msg}")
lib.fulfill_log(future_handle)
else:
print(f" [WARN] unknown effect {effect}, re-polling")
passed = 0
failed = 0
def check(name, got, want):
global passed, failed
if got == want:
print(f" PASS {name}: got {got}")
passed += 1
else:
print(f" FAIL {name}: got {got}, want {want}")
failed += 1
def main():
global passed, failed
print("=== Python <-> Rust FFI (effects) e2e test ===")
print()
counter = lib.effects_counter_new(10)
fut = lib.counter_value_at_now(counter)
val, ts = poll_with_effects(fut)
lib.effects_future_free(fut)
check("value_at_now value", val, 10)
if ts > 0:
print(f" PASS value_at_now timestamp: {ts} ms")
passed += 1
else:
print(f" FAIL value_at_now timestamp: got {ts}, want > 0")
failed += 1
fut = lib.counter_add_with_log(counter, 32)
val, _ = poll_with_effects(fut)
lib.effects_future_free(fut)
check("add_with_log(10, 32)", val, 42)
lib.effects_counter_free(counter)
counter = lib.effects_counter_new(100)
fut = lib.counter_value_at_now(counter)
val, ts = poll_with_effects(fut)
lib.effects_future_free(fut)
check("value_at_now(100) value", val, 100)
if ts > 0:
print(f" PASS value_at_now(100) timestamp: {ts} ms")
passed += 1
else:
print(f" FAIL value_at_now(100) timestamp: got {ts}, want > 0")
failed += 1
fut = lib.counter_add_with_log(counter, 900)
val, _ = poll_with_effects(fut)
lib.effects_future_free(fut)
check("add_with_log(100, 900)", val, 1000)
lib.effects_counter_free(counter)
print()
print(f"--- {passed} passed, {failed} failed ---")
if failed > 0:
sys.exit(1)
if __name__ == "__main__":
main()