Skip to content

Commit bd1e610

Browse files
committed
Document socket FD handoff from Erlang to Python
1 parent 8639930 commit bd1e610

File tree

2 files changed

+141
-1
lines changed

2 files changed

+141
-1
lines changed

docs/migration.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,33 @@ serve(sock, EchoProtocol)
324324

325325
See [Reactor](reactor.md) for full documentation.
326326

327+
### Socket FD Handoff
328+
329+
Pass socket file descriptors directly from Erlang to Python for high-performance I/O:
330+
331+
```erlang
332+
%% Erlang: Accept connection and get fd
333+
{ok, ClientSock} = gen_tcp:accept(ListenSock),
334+
{ok, Fd} = inet:getfd(ClientSock),
335+
336+
%% Hand off to Python reactor (Erlang releases ownership)
337+
py_reactor_context:handoff(Fd, #{type => tcp}).
338+
339+
%% Or pass fd to asyncio-based Python code
340+
Ctx = py:context(1),
341+
py:call(Ctx, my_handler, handle_fd, [Fd]).
342+
```
343+
344+
```python
345+
# Python: Use fd with reactor or asyncio
346+
import socket
347+
sock = socket.socket(fileno=fd) # Takes ownership
348+
sock.setblocking(False)
349+
# ... use with asyncio or reactor
350+
```
351+
352+
See [Reactor](reactor.md#passing-sockets-from-erlang-to-python) for details.
353+
327354
### `erlang.send()` for Fire-and-Forget Messages
328355

329356
Send messages directly to Erlang processes without waiting:

docs/reactor.md

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,118 @@ class SimpleHTTPProtocol(reactor.Protocol):
289289
reactor.set_protocol_factory(SimpleHTTPProtocol)
290290
```
291291

292+
## Passing Sockets from Erlang to Python
293+
294+
### Method 1: Socket FD Handoff to Reactor
295+
296+
The most efficient way is to hand off the socket's file descriptor directly:
297+
298+
```erlang
299+
%% Erlang: Accept and hand off to Python reactor
300+
{ok, ClientSock} = gen_tcp:accept(ListenSock),
301+
{ok, {Addr, Port}} = inet:peername(ClientSock),
302+
303+
%% Get the raw file descriptor
304+
{ok, Fd} = inet:getfd(ClientSock),
305+
306+
%% Hand off to Python - Erlang no longer owns this socket
307+
py_reactor_context:handoff(Fd, #{
308+
addr => inet:ntoa(Addr),
309+
port => Port,
310+
type => tcp
311+
}).
312+
```
313+
314+
```python
315+
# Python: Protocol handles the fd
316+
import erlang.reactor as reactor
317+
318+
class MyProtocol(reactor.Protocol):
319+
def data_received(self, data):
320+
# self.fd is the socket fd from Erlang
321+
self.write_buffer.extend(b"Got: " + data)
322+
return "write_pending"
323+
324+
reactor.set_protocol_factory(MyProtocol)
325+
```
326+
327+
### Method 2: Pass Socket FD to asyncio
328+
329+
For asyncio-based code, pass the fd and wrap it in Python:
330+
331+
```erlang
332+
%% Erlang: Get fd and pass to Python
333+
{ok, ClientSock} = gen_tcp:accept(ListenSock),
334+
{ok, Fd} = inet:getfd(ClientSock),
335+
336+
%% Call Python with the fd
337+
Ctx = py:context(1),
338+
py:call(Ctx, my_handler, handle_connection, [Fd]).
339+
```
340+
341+
```python
342+
# Python: Wrap fd in asyncio
343+
import asyncio
344+
import socket
345+
346+
async def handle_connection(fd: int):
347+
# Create socket from fd (Python takes ownership)
348+
sock = socket.socket(fileno=fd)
349+
sock.setblocking(False)
350+
351+
# Use asyncio streams
352+
reader, writer = await asyncio.open_connection(sock=sock)
353+
354+
data = await reader.read(1024)
355+
writer.write(b"Echo: " + data)
356+
await writer.drain()
357+
writer.close()
358+
await writer.wait_closed()
359+
360+
def handle_connection_sync(fd: int):
361+
"""Sync wrapper for Erlang call."""
362+
asyncio.run(handle_connection(fd))
363+
```
364+
365+
### Method 3: Pass Socket Object via Pickle (Not Recommended)
366+
367+
For simple cases, you can pickle socket info, but this is less efficient:
368+
369+
```erlang
370+
%% Erlang: Pass connection info
371+
{ok, {Addr, Port}} = inet:peername(ClientSock),
372+
py:call(Ctx, my_handler, connect_to, [Addr, Port]).
373+
```
374+
375+
```python
376+
# Python: Create new connection (less efficient - new socket)
377+
import socket
378+
379+
def connect_to(addr: str, port: int):
380+
sock = socket.create_connection((addr, port))
381+
# ... use socket
382+
```
383+
384+
### Important: Socket Ownership
385+
386+
When passing an fd from Erlang to Python:
387+
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
391+
392+
```erlang
393+
%% WRONG - double close
394+
{ok, Fd} = inet:getfd(ClientSock),
395+
py_reactor_context:handoff(Fd, #{}),
396+
gen_tcp:close(ClientSock). %% BAD: Python will also close
397+
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
402+
```
403+
292404
## Integration with Erlang
293405

294406
### From Erlang: Starting a Reactor Server
@@ -300,7 +412,8 @@ reactor.set_protocol_factory(SimpleHTTPProtocol)
300412

301413
start(Port) ->
302414
%% Set up the Python protocol factory first
303-
ok = py:exec(<<"
415+
Ctx = py:context(1),
416+
ok = py:exec(Ctx, <<"
304417
import erlang.reactor as reactor
305418
from my_protocols import MyProtocol
306419
reactor.set_protocol_factory(MyProtocol)

0 commit comments

Comments
 (0)