Skip to content

Commit 8fa255e

Browse files
committed
Auto-dup fd in reactor_register_fd for safe socket handoff
- reactor_register_fd now calls dup() on the fd automatically - Sets owns_fd=true so the duplicated fd is closed on cleanup - Users can safely close Erlang's socket after handoff - Updated documentation to reflect automatic dup()
1 parent bd1e610 commit 8fa255e

File tree

3 files changed

+46
-24
lines changed

3 files changed

+46
-24
lines changed

c_src/py_event_loop.c

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2904,14 +2904,22 @@ ERL_NIF_TERM nif_reactor_register_fd(ErlNifEnv *env, int argc,
29042904
return make_error(env, "invalid_pid");
29052905
}
29062906

2907+
/* Duplicate the fd so we own our copy independent of Erlang's socket.
2908+
* This prevents issues when Erlang's socket is garbage collected. */
2909+
int dup_fd = dup(fd);
2910+
if (dup_fd < 0) {
2911+
return make_error(env, "dup_failed");
2912+
}
2913+
29072914
/* Allocate fd resource */
29082915
fd_resource_t *fd_res = enif_alloc_resource(FD_RESOURCE_TYPE,
29092916
sizeof(fd_resource_t));
29102917
if (fd_res == NULL) {
2918+
close(dup_fd);
29112919
return make_error(env, "alloc_failed");
29122920
}
29132921

2914-
fd_res->fd = fd;
2922+
fd_res->fd = dup_fd; /* Use duplicated fd */
29152923
fd_res->read_callback_id = 0; /* Not used for reactor mode */
29162924
fd_res->write_callback_id = 0;
29172925
fd_res->owner_pid = owner_pid;
@@ -2922,7 +2930,7 @@ ERL_NIF_TERM nif_reactor_register_fd(ErlNifEnv *env, int argc,
29222930
/* Initialize lifecycle management */
29232931
atomic_store(&fd_res->closing_state, FD_STATE_OPEN);
29242932
fd_res->monitor_active = false;
2925-
fd_res->owns_fd = false; /* Erlang owns the socket via gen_tcp */
2933+
fd_res->owns_fd = true; /* We own the duplicated fd */
29262934

29272935
/* Monitor owner process for cleanup on death */
29282936
if (enif_monitor_process(env, fd_res, &owner_pid,

docs/migration.md

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -326,30 +326,40 @@ See [Reactor](reactor.md) for full documentation.
326326

327327
### Socket FD Handoff
328328

329-
Pass socket file descriptors directly from Erlang to Python for high-performance I/O:
329+
Pass socket file descriptors directly from Erlang to Python for high-performance I/O.
330+
331+
The reactor automatically `dup()`s the fd, so you can safely close Erlang's socket:
330332

331333
```erlang
332-
%% Erlang: Accept connection and get fd
334+
%% Erlang: Accept and hand off to reactor
333335
{ok, ClientSock} = gen_tcp:accept(ListenSock),
334336
{ok, Fd} = inet:getfd(ClientSock),
335337

336-
%% Hand off to Python reactor (Erlang releases ownership)
337-
py_reactor_context:handoff(Fd, #{type => tcp}).
338+
%% Hand off fd - reactor dup()s it automatically
339+
py_reactor_context:handoff(Fd, #{type => tcp}),
340+
341+
%% Safe to close Erlang's socket
342+
gen_tcp:close(ClientSock).
343+
```
338344

339-
%% Or pass fd to asyncio-based Python code
345+
For direct asyncio usage (not via reactor), dup manually:
346+
347+
```erlang
348+
%% For direct asyncio - dup the fd yourself
349+
{ok, DupFd} = prim_file:dup(Fd),
340350
Ctx = py:context(1),
341-
py:call(Ctx, my_handler, handle_fd, [Fd]).
351+
py:call(Ctx, my_handler, handle_fd, [DupFd]).
342352
```
343353

344354
```python
345-
# Python: Use fd with reactor or asyncio
355+
# Python: Use fd with asyncio
346356
import socket
347-
sock = socket.socket(fileno=fd) # Takes ownership
357+
sock = socket.socket(fileno=fd)
348358
sock.setblocking(False)
349-
# ... use with asyncio or reactor
359+
# ... use with asyncio
350360
```
351361

352-
See [Reactor](reactor.md#passing-sockets-from-erlang-to-python) for details.
362+
See [Reactor](reactor.md#socket-ownership) for details.
353363

354364
### `erlang.send()` for Fire-and-Forget Messages
355365

docs/reactor.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -381,26 +381,30 @@ def connect_to(addr: str, port: int):
381381
# ... use socket
382382
```
383383

384-
### Important: Socket Ownership
384+
### Socket Ownership
385385

386-
When passing an fd from Erlang to Python:
386+
The reactor automatically duplicates (`dup()`) the file descriptor when you call
387+
`py_reactor_context:handoff/2`. This means:
387388

388-
1. **Erlang releases ownership**: After `inet:getfd/1`, don't use the Erlang socket
389-
2. **Python takes ownership**: Close the socket in Python when done
390-
3. **Don't double-close**: Either Erlang or Python closes, not both
389+
1. **Erlang keeps its socket** - You can close it whenever convenient
390+
2. **Python gets its own fd copy** - Independent of Erlang's lifecycle
391+
3. **No double-close issues** - Each side manages its own fd
391392

392393
```erlang
393-
%% WRONG - double close
394+
%% Simple and safe - reactor handles dup() internally
395+
{ok, ClientSock} = gen_tcp:accept(ListenSock),
394396
{ok, Fd} = inet:getfd(ClientSock),
395-
py_reactor_context:handoff(Fd, #{}),
396-
gen_tcp:close(ClientSock). %% BAD: Python will also close
397397

398-
%% RIGHT - let Python handle it
399-
{ok, Fd} = inet:getfd(ClientSock),
400-
py_reactor_context:handoff(Fd, #{}).
401-
%% Don't close ClientSock - Python owns it now
398+
%% Hand off fd - reactor will dup() it automatically
399+
py_reactor_context:handoff(Fd, #{type => tcp}),
400+
401+
%% Safe to close Erlang's socket immediately
402+
gen_tcp:close(ClientSock).
402403
```
403404

405+
The reactor will close its duplicated fd when the protocol completes or the
406+
connection is closed.
407+
404408
## Integration with Erlang
405409

406410
### From Erlang: Starting a Reactor Server

0 commit comments

Comments
 (0)