Skip to content

Commit 4373a8e

Browse files
committed
Yay WSGI app
1 parent a75f07f commit 4373a8e

10 files changed

+191
-35
lines changed

Makefile

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ small: clean
3232
CFLAGS='-Os' make
3333

3434
bjoernmodule:
35-
$(CC) $(CPPFLAGS) $(LDFLAGS) $(objects) $(HTTP_PARSER_OBJ) -o $(BUILD_DIR)/_bjoern.so
36-
python -c "import _bjoern"
35+
$(CC) $(CPPFLAGS) $(LDFLAGS) $(objects) $(HTTP_PARSER_OBJ) -o $(BUILD_DIR)/bjoern.so
36+
python -c "import bjoern"
3737

3838
again: clean all
3939

@@ -64,4 +64,4 @@ test:
6464

6565
callgrind:
6666
cd _build
67-
valgrind --tool=callgrind python -c 'import _bjoern'
67+
valgrind --tool=callgrind python -c 'import bjoern'

bjoern/bjoernmodule.c

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
#include <Python.h>
22
#include "server.h"
3+
#include "bjoernmodule.h"
4+
5+
static PyObject*
6+
run(PyObject* self, PyObject* args)
7+
{
8+
const char* host;
9+
size_t port;
10+
11+
if(!PyArg_ParseTuple(args, "Osi", &wsgi_app, &host, &port))
12+
return NULL;
13+
14+
server_run(host, port);
15+
Py_RETURN_NONE;
16+
}
317

418
static PyMethodDef Bjoern_FunctionTable[] = {
19+
{"run", run, METH_VARARGS, NULL},
520
{NULL, NULL, 0, NULL}
621
};
722

823

9-
PyMODINIT_FUNC init_bjoern()
24+
PyMODINIT_FUNC initbjoern()
1025
{
11-
Py_InitModule("_bjoern", Bjoern_FunctionTable);
12-
server_run("0.0.0.0", 8080);
26+
Py_InitModule("bjoern", Bjoern_FunctionTable);
1327
}

bjoern/bjoernmodule.h

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
PyObject* wsgi_app;

bjoern/common.h

+16-2
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,26 @@
77
#include <stdbool.h>
88
#include <string.h>
99

10-
#define GIL_LOCK1() PyGILState_STATE _gilstate = PyGILState_Ensure()
11-
#define GIL_UNLOCK1() PyGILState_Release(_gilstate)
10+
#define GIL_LOCK(n) PyGILState_STATE _gilstate_##n = PyGILState_Ensure()
11+
#define GIL_UNLOCK(n) PyGILState_Release(_gilstate_##n)
1212

1313
#define LENGTH(array) (sizeof(array)/sizeof(array[0]))
1414
#define ADDR_FROM_MEMBER(ptr, strct, mem) (strct*)((size_t)ptr - (size_t)(&(((strct*)NULL)->mem)));
1515

16+
#define TYPECHECK2(what, check_type, print_type, errmsg_name, failure_retval) \
17+
if(!check_type##_Check(what)) { \
18+
PyErr_Format(\
19+
PyExc_TypeError, \
20+
errmsg_name " must be of type %s, not %s", \
21+
print_type##_Type.tp_name, \
22+
Py_TYPE(what ? what : Py_None)->tp_name \
23+
); \
24+
return failure_retval; \
25+
}
26+
#define TYPECHECK(what, type, ...) TYPECHECK2(what, type, type, __VA_ARGS__);
27+
28+
typedef PyObject* PyKeywordFunc(PyObject* self, PyObject* args, PyObject *kwargs);
29+
1630
typedef enum {
1731
HTTP_BAD_REQUEST = 400,
1832
HTTP_SERVER_ERROR = 500

bjoern/request.h

+9-3
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ typedef enum {
1010
REQUEST_READING,
1111
REQUEST_PARSE_ERROR,
1212
REQUEST_PARSE_DONE,
13-
REQUEST_WSGI_RESPONSE,
14-
REQUEST_WSGI_DONE
13+
REQUEST_WSGI_GENERAL_RESPONSE,
14+
REQUEST_WSGI_STRING_RESPONSE,
15+
REQUEST_WSGI_FILE_RESPONSE,
16+
REQUEST_WSGI_ITER_RESPONSE
1517
} request_state;
1618

1719
typedef struct {
@@ -28,8 +30,12 @@ typedef struct {
2830
ev_io ev_watcher;
2931

3032
bj_parser parser;
31-
void* response;
3233
PyObject* headers;
34+
35+
void* response;
36+
PyObject* response_headers;
37+
PyObject* status;
38+
PyObject* response_curiter;
3339
} Request;
3440

3541
Request* Request_new(int client_fd);

bjoern/server.c

+12-7
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
}
2121

2222

23-
static int sockfd;
23+
static int sockfd;
2424

2525
typedef void ev_io_callback(struct ev_loop*, ev_io*, const int);
2626
static ev_io_callback ev_io_on_request;
@@ -104,20 +104,25 @@ ev_io_on_read(struct ev_loop* mainloop, ev_io* watcher, const int events)
104104

105105
request->state = REQUEST_READING;
106106

107+
GIL_LOCK(0);
108+
107109
HANDLE_IO_ERROR(read_bytes,
108110
/* on fatal error */ set_error(request, HTTP_SERVER_ERROR); goto out
109111
);
110112

111-
GIL_LOCK1();
112113
Request_parse(request, read_buf, read_bytes);
113-
GIL_UNLOCK1();
114+
114115
switch(request->state) {
116+
115117
case REQUEST_PARSE_ERROR:
116118
set_error(request, HTTP_BAD_REQUEST);
117119
break;
118120
case REQUEST_PARSE_DONE:
119-
if(!wsgi_call_application(request))
121+
if(!wsgi_call_application(request)) {
122+
if(PyErr_Occurred())
123+
PyErr_Print();
120124
set_error(request, HTTP_SERVER_ERROR);
125+
}
121126
break;
122127
default:
123128
assert(request->state == REQUEST_READING);
@@ -131,17 +136,17 @@ ev_io_on_read(struct ev_loop* mainloop, ev_io* watcher, const int events)
131136
ev_io_start(mainloop, &request->ev_watcher);
132137

133138
again:
139+
GIL_UNLOCK(0);
134140
return;
135141
}
136142

137143
static void
138144
ev_io_on_write(struct ev_loop* mainloop, ev_io* watcher, const int events)
139145
{
140146
Request* request = ADDR_FROM_MEMBER(watcher, Request, ev_watcher);
141-
if(request->state == REQUEST_WSGI_RESPONSE) {
147+
if(request->state > REQUEST_WSGI_GENERAL_RESPONSE) {
142148
/* request->response is something that the WSGI application returned */
143-
wsgi_send_response(request);
144-
if(request->state != REQUEST_WSGI_DONE)
149+
if(!wsgi_send_response(request))
145150
return; /* come around again */
146151
} else {
147152
/* request->response is a C-string */

bjoern/wsgi.c

+130-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,143 @@
1+
#include "common.h"
12
#include "server.h"
3+
#include "bjoernmodule.h"
24
#include "wsgi.h"
35

6+
static PyKeywordFunc start_response;
7+
static bool wsgi_sendfile(Request*);
8+
static bool wsgi_senditer(Request*);
9+
10+
typedef struct {
11+
PyObject_HEAD
12+
Request* request;
13+
} StartResponse;
14+
static PyTypeObject StartResponse_Type;
15+
16+
417
bool
518
wsgi_call_application(Request* request)
619
{
7-
request->response = PyString_FromString("Hello World!\n");
8-
request->state = REQUEST_WSGI_RESPONSE;
20+
StartResponse* start_response = PyObject_NEW(StartResponse, &StartResponse_Type);
21+
start_response->request = request;
22+
PyObject* args = PyTuple_Pack(/* size */ 2, request->headers, start_response);
23+
24+
GIL_LOCK(0);
25+
PyObject* retval = PyObject_CallObject(wsgi_app, args);
26+
GIL_UNLOCK(0);
27+
28+
Py_DECREF(start_response);
29+
if(retval == NULL)
30+
return false;
31+
32+
if(PyFile_Check(retval)) {
33+
request->state = REQUEST_WSGI_FILE_RESPONSE;
34+
request->response = retval;
35+
return true;
36+
}
37+
38+
if(PyString_Check(retval)) {
39+
request->state = REQUEST_WSGI_STRING_RESPONSE;
40+
request->response = retval;
41+
return true;
42+
}
43+
44+
PyObject* iter = PyObject_GetIter(retval);
45+
TYPECHECK2(iter, PyIter, PySeqIter, "wsgi application return value", false);
46+
47+
Py_DECREF(retval);
48+
request->state = REQUEST_WSGI_ITER_RESPONSE;
49+
request->response = iter;
50+
/* Get the first item of the iterator, because that may execute code that
51+
* invokes `start_response` (which might not have been invoked yet).
52+
* Think of the following scenario:
53+
*
54+
* def app(environ, start_response):
55+
* start_response('200 Ok', ...)
56+
* yield 'Hello World'
57+
*
58+
* That would make `app` return an iterator (more precisely, a generator).
59+
* Unfortunately, `start_response` wouldn't be called until the first item
60+
* of that iterator is requested; `start_response` however has to be called
61+
* _before_ the wsgi body is sent, because it passes the HTTP headers.
62+
*/
63+
request->response_curiter = PyIter_Next(iter);
64+
965
return true;
1066
}
1167

1268
bool
1369
wsgi_send_response(Request* request)
1470
{
15-
sendall(request, PyString_AS_STRING(request->response), PyString_GET_SIZE(request->response));
16-
request->state = REQUEST_WSGI_DONE;
17-
return true;
71+
switch(request->state) {
72+
case REQUEST_WSGI_STRING_RESPONSE:
73+
sendall(
74+
request,
75+
PyString_AS_STRING(request->response),
76+
PyString_GET_SIZE(request->response)
77+
);
78+
return true;
79+
80+
case REQUEST_WSGI_FILE_RESPONSE:
81+
return wsgi_sendfile(request);
82+
83+
case REQUEST_WSGI_ITER_RESPONSE:
84+
return wsgi_senditer(request);
85+
86+
default:
87+
assert(0);
88+
}
1889
}
90+
91+
static bool
92+
wsgi_sendfile(Request* request)
93+
{
94+
assert(0);
95+
}
96+
97+
static bool
98+
wsgi_senditer(Request* request)
99+
{
100+
if(request->response_curiter == NULL)
101+
return true;
102+
103+
TYPECHECK(request->response_curiter, PyString, "wsgi iterable items", true);
104+
sendall(
105+
request,
106+
PyString_AS_STRING(request->response_curiter),
107+
PyString_GET_SIZE(request->response_curiter)
108+
);
109+
110+
GIL_LOCK(0);
111+
request->response_curiter = PyIter_Next(request->response);
112+
GIL_UNLOCK(0);
113+
return request->response_curiter != NULL;
114+
}
115+
116+
117+
118+
static PyObject*
119+
start_response(PyObject* self, PyObject* args, PyObject* kwargs)
120+
{
121+
Request* req = ((StartResponse*)self)->request;
122+
123+
if(!PyArg_UnpackTuple(args,
124+
/* Python function name */ "start_response",
125+
/* min args */ 2, /* max args */ 2,
126+
&req->status,
127+
&req->response_headers
128+
))
129+
return NULL;
130+
131+
TYPECHECK(req->status, PyString, "start_response argument 1", NULL);
132+
TYPECHECK(req->response_headers, PyList, "start_response argument 2", NULL);
133+
134+
Py_RETURN_NONE;
135+
}
136+
137+
static PyTypeObject StartResponse_Type = {
138+
PyVarObject_HEAD_INIT(NULL, 0)
139+
"start_response",
140+
sizeof(StartResponse),
141+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
142+
start_response /* __call__ */
143+
};

bjoern/wsgi.h

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#include "common.h"
21
#include "request.h"
32

43
bool wsgi_call_application(Request*);

stuff/wsgitest_config.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,4 @@
44
SERVER_BOOT_DURATION = 0.1
55

66
def run_server(app, host, port):
7-
if bjoern.HAVE_ROUTING:
8-
bjoern.route('.*')(app)
9-
bjoern.run(host, port)
10-
else:
11-
bjoern.run(app, host, port)
7+
bjoern.run(app, host, port)

tests/hello.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22

33
def wsgi_app(env, start_response):
44
start_response('200 abc', [])
5-
return []
5+
return ['Hello World']
66

7-
if bjoern.HAVE_ROUTING:
8-
bjoern.route('.*')(wsgi_app)
9-
bjoern.run('0.0.0.0', 8080)
10-
else:
11-
bjoern.run(wsgi_app, '0.0.0.0', 8080)
7+
bjoern.run(wsgi_app, '0.0.0.0', 8080)

0 commit comments

Comments
 (0)