@@ -56,6 +56,180 @@ ERL_NIF_TERM ATOM_ASGI_HEADERS;
5656ERL_NIF_TERM ATOM_ASGI_CLIENT ;
5757ERL_NIF_TERM ATOM_ASGI_QUERY_STRING ;
5858
59+ /* Resource type for zero-copy body buffers */
60+ ErlNifResourceType * ASGI_BUFFER_RESOURCE_TYPE = NULL ;
61+
62+ /* ============================================================================
63+ * Zero-Copy Buffer Resource
64+ * ============================================================================
65+ * A NIF resource that holds binary data and can be exposed to Python via
66+ * the buffer protocol. This enables zero-copy access within Python while
67+ * ensuring the data stays valid as long as Python holds references.
68+ */
69+
70+ typedef struct {
71+ unsigned char * data ; /* Binary data */
72+ size_t size ; /* Data size */
73+ int ref_count ; /* Python reference count for buffer views */
74+ } asgi_buffer_resource_t ;
75+
76+ /**
77+ * @brief Destructor for buffer resources
78+ */
79+ static void asgi_buffer_resource_dtor (ErlNifEnv * env , void * obj ) {
80+ (void )env ;
81+ asgi_buffer_resource_t * buf = (asgi_buffer_resource_t * )obj ;
82+ if (buf -> data != NULL ) {
83+ enif_free (buf -> data );
84+ buf -> data = NULL ;
85+ }
86+ }
87+
88+ /* ============================================================================
89+ * Python Buffer Object
90+ * ============================================================================
91+ * A Python object that wraps an ASGI buffer resource and exposes it via
92+ * the buffer protocol for zero-copy access.
93+ */
94+
95+ typedef struct {
96+ PyObject_HEAD
97+ asgi_buffer_resource_t * resource ; /* NIF resource (we hold a reference) */
98+ void * resource_ref ; /* For releasing the resource */
99+ } AsgiBufferObject ;
100+
101+ static PyTypeObject AsgiBufferType ; /* Forward declaration */
102+
103+ /**
104+ * @brief Release buffer callback for Python buffer protocol
105+ */
106+ static void AsgiBuffer_releasebuffer (PyObject * obj , Py_buffer * view ) {
107+ (void )view ;
108+ AsgiBufferObject * self = (AsgiBufferObject * )obj ;
109+ if (self -> resource != NULL ) {
110+ self -> resource -> ref_count -- ;
111+ }
112+ }
113+
114+ /**
115+ * @brief Get buffer callback for Python buffer protocol
116+ */
117+ static int AsgiBuffer_getbuffer (PyObject * obj , Py_buffer * view , int flags ) {
118+ AsgiBufferObject * self = (AsgiBufferObject * )obj ;
119+
120+ if (self -> resource == NULL || self -> resource -> data == NULL ) {
121+ PyErr_SetString (PyExc_BufferError , "Buffer has been released" );
122+ return -1 ;
123+ }
124+
125+ /* Fill in the buffer structure */
126+ view -> obj = obj ;
127+ view -> buf = self -> resource -> data ;
128+ view -> len = self -> resource -> size ;
129+ view -> readonly = 1 ;
130+ view -> itemsize = 1 ;
131+ view -> format = (flags & PyBUF_FORMAT ) ? "B" : NULL ;
132+ view -> ndim = 1 ;
133+ view -> shape = (flags & PyBUF_ND ) ? & view -> len : NULL ;
134+ view -> strides = (flags & PyBUF_STRIDES ) ? & view -> itemsize : NULL ;
135+ view -> suboffsets = NULL ;
136+ view -> internal = NULL ;
137+
138+ self -> resource -> ref_count ++ ;
139+ Py_INCREF (obj );
140+
141+ return 0 ;
142+ }
143+
144+ static PyBufferProcs AsgiBuffer_as_buffer = {
145+ .bf_getbuffer = AsgiBuffer_getbuffer ,
146+ .bf_releasebuffer = AsgiBuffer_releasebuffer ,
147+ };
148+
149+ /**
150+ * @brief Deallocate buffer object
151+ */
152+ static void AsgiBuffer_dealloc (AsgiBufferObject * self ) {
153+ if (self -> resource_ref != NULL ) {
154+ enif_release_resource (self -> resource_ref );
155+ self -> resource_ref = NULL ;
156+ self -> resource = NULL ;
157+ }
158+ Py_TYPE (self )-> tp_free ((PyObject * )self );
159+ }
160+
161+ /**
162+ * @brief Get length of buffer
163+ */
164+ static Py_ssize_t AsgiBuffer_length (AsgiBufferObject * self ) {
165+ if (self -> resource == NULL ) {
166+ return 0 ;
167+ }
168+ return (Py_ssize_t )self -> resource -> size ;
169+ }
170+
171+ /**
172+ * @brief Get bytes representation
173+ */
174+ static PyObject * AsgiBuffer_bytes (AsgiBufferObject * self ) {
175+ if (self -> resource == NULL || self -> resource -> data == NULL ) {
176+ return PyBytes_FromStringAndSize ("" , 0 );
177+ }
178+ return PyBytes_FromStringAndSize ((char * )self -> resource -> data ,
179+ self -> resource -> size );
180+ }
181+
182+ static PyMethodDef AsgiBuffer_methods [] = {
183+ {"__bytes__" , (PyCFunction )AsgiBuffer_bytes , METH_NOARGS ,
184+ "Return bytes copy of buffer" },
185+ {NULL }
186+ };
187+
188+ static PySequenceMethods AsgiBuffer_as_sequence = {
189+ .sq_length = (lenfunc )AsgiBuffer_length ,
190+ };
191+
192+ static PyTypeObject AsgiBufferType = {
193+ PyVarObject_HEAD_INIT (NULL , 0 )
194+ .tp_name = "erlang_python.AsgiBuffer" ,
195+ .tp_doc = "Zero-copy ASGI body buffer" ,
196+ .tp_basicsize = sizeof (AsgiBufferObject ),
197+ .tp_itemsize = 0 ,
198+ .tp_flags = Py_TPFLAGS_DEFAULT ,
199+ .tp_dealloc = (destructor )AsgiBuffer_dealloc ,
200+ .tp_as_buffer = & AsgiBuffer_as_buffer ,
201+ .tp_as_sequence = & AsgiBuffer_as_sequence ,
202+ .tp_methods = AsgiBuffer_methods ,
203+ };
204+
205+ /**
206+ * @brief Create an AsgiBuffer from a NIF resource
207+ */
208+ static PyObject * AsgiBuffer_from_resource (asgi_buffer_resource_t * resource ,
209+ void * resource_ref ) {
210+ AsgiBufferObject * obj = PyObject_New (AsgiBufferObject , & AsgiBufferType );
211+ if (obj == NULL ) {
212+ return NULL ;
213+ }
214+
215+ obj -> resource = resource ;
216+ obj -> resource_ref = resource_ref ;
217+ /* Keep the resource alive */
218+ enif_keep_resource (resource_ref );
219+
220+ return (PyObject * )obj ;
221+ }
222+
223+ /**
224+ * @brief Initialize the AsgiBuffer type (call during module init)
225+ */
226+ static int AsgiBuffer_init_type (void ) {
227+ if (PyType_Ready (& AsgiBufferType ) < 0 ) {
228+ return -1 ;
229+ }
230+ return 0 ;
231+ }
232+
59233/**
60234 * @brief Initialize a single interpreter state
61235 */
@@ -730,6 +904,11 @@ static int asgi_scope_init(void) {
730904 return 0 ;
731905 }
732906
907+ /* Initialize the AsgiBuffer Python type for zero-copy body handling */
908+ if (AsgiBuffer_init_type () < 0 ) {
909+ return -1 ;
910+ }
911+
733912 /* Initialize per-interpreter state for current interpreter */
734913 asgi_interp_state_t * state = get_asgi_interp_state ();
735914 if (!state ) {
@@ -1330,20 +1509,53 @@ static PyObject *asgi_binary_to_buffer(ErlNifEnv *env, ERL_NIF_TERM binary) {
13301509 return NULL ;
13311510 }
13321511
1333- /* For small bodies, copy to bytes */
1512+ /* For small bodies, copy to bytes - overhead of resource not worth it */
13341513 if (bin .size < ASGI_ZERO_COPY_THRESHOLD ) {
13351514 return PyBytes_FromStringAndSize ((char * )bin .data , bin .size );
13361515 }
13371516
1338- /* For large bodies, create a memoryview
1339- * Note: This requires the Erlang binary to stay valid during processing.
1340- * The memoryview points directly to the binary's memory. */
1517+ /* For large bodies, use resource-backed buffer for zero-copy Python access.
1518+ *
1519+ * This approach:
1520+ * 1. Copies data once into a NIF resource
1521+ * 2. Resource stays alive as long as Python holds references
1522+ * 3. Python can slice/view the buffer without additional copies
1523+ * 4. Works safely with async code since resource lifetime is managed
1524+ */
1525+ if (ASGI_BUFFER_RESOURCE_TYPE == NULL ) {
1526+ /* Fallback if resource type not initialized */
1527+ return PyBytes_FromStringAndSize ((char * )bin .data , bin .size );
1528+ }
1529+
1530+ /* Allocate resource */
1531+ asgi_buffer_resource_t * resource = enif_alloc_resource (
1532+ ASGI_BUFFER_RESOURCE_TYPE , sizeof (asgi_buffer_resource_t ));
1533+ if (resource == NULL ) {
1534+ PyErr_NoMemory ();
1535+ return NULL ;
1536+ }
1537+
1538+ /* Allocate and copy data */
1539+ resource -> data = enif_alloc (bin .size );
1540+ if (resource -> data == NULL ) {
1541+ enif_release_resource (resource );
1542+ PyErr_NoMemory ();
1543+ return NULL ;
1544+ }
1545+ memcpy (resource -> data , bin .data , bin .size );
1546+ resource -> size = bin .size ;
1547+ resource -> ref_count = 0 ;
1548+
1549+ /* Create Python buffer object wrapping the resource */
1550+ PyObject * buffer = AsgiBuffer_from_resource (resource , resource );
1551+ /* Release our reference - Python now owns it */
1552+ enif_release_resource (resource );
1553+
1554+ if (buffer == NULL ) {
1555+ return NULL ;
1556+ }
13411557
1342- /* Create a bytes object that we'll use as the buffer source.
1343- * For true zero-copy, we'd need to implement a custom buffer object
1344- * that wraps the Erlang binary. For now, we still copy but use
1345- * efficient memoryview semantics for subsequent processing. */
1346- return PyBytes_FromStringAndSize ((char * )bin .data , bin .size );
1558+ return buffer ;
13471559}
13481560
13491561/* ============================================================================
0 commit comments