The Symcall plugin

The symcall plugin (modifier 18) is a convenience plugin allowing you to write native uWSGI request handlers without the need of developing a full uWSGI plugin.

You tell it which symbol to load on startup and then it will run it at every request.

Note

The “symcall” plugin is built-in by default in standard build profiles.

Step 1: preparing the environment

The uWSGI binary by itself allows you to develop plugins and libraries without the need of external development packages or headers.

The first step is getting the uwsgi.h C/C++ header:

  1. uwsgi --dot-h > uwsgi.h

Now, in the current directory, we have a fresh uwsgi.h ready to be included.

Step 2: our first request handler:

Our C handler will print the REMOTE_ADDR value with a couple of HTTP headers.

(call it mysym.c or whatever you want/need)

  1. #include "uwsgi.h"
  2.  
  3. int mysym_function(struct wsgi_request *wsgi_req) {
  4.  
  5. // read request variables
  6. if (uwsgi_parse_vars(wsgi_req)) {
  7. return -1;
  8. }
  9.  
  10. // get REMOTE_ADDR
  11. uint16_t vlen = 0;
  12. char *v = uwsgi_get_var(wsgi_req, "REMOTE_ADDR", 11, &vlen);
  13.  
  14. // send status
  15. if (uwsgi_response_prepare_headers(wsgi_req, "200 OK", 6)) return -1;
  16. // send content_type
  17. if (uwsgi_response_add_content_type(wsgi_req, "text/plain", 10)) return -1;
  18. // send a custom header
  19. if (uwsgi_response_add_header(wsgi_req, "Foo", 3, "Bar", 3)) return -1;
  20.  
  21. // send the body
  22. if (uwsgi_response_write_body_do(wsgi_req, v, vlen)) return -1;
  23.  
  24. return UWSGI_OK;
  25. }

Step 3: building our code as a shared library

The uwsgi.h file is an ifdef hell (so it’s probably better not to look at it too closely).

Fortunately the uwsgi binary exposes all of the required CFLAGS via the –cflags option.

We can build our library in one shot:

  1. gcc -fPIC -shared -o mysym.so `uwsgi --cflags` mysym.c

you now have the mysym.so library ready to be loaded in uWSGI

Final step: map the symcall plugin to the mysym_function symbol

  1. uwsgi --dlopen ./mysym.so --symcall mysym_function --http-socket :9090 --http-socket-modifier1 18

With —dlopen we load a shared library in the uWSGI process address space.

The —symcall option allows us to specify which symbol to call when modifier1 18 is in place

We bind the instance to HTTP socket 9090 forcing modifier1 18.

Hooks and symcall unleashed: a TCL handler

We want to write a request handler running the following TCL script (foo.tcl) every time:

  1. # call it foo.tcl
  2. proc request_handler { remote_addr path_info query_string } {
  3. set upper_pathinfo [string toupper $path_info]
  4. return "Hello $remote_addr $upper_pathinfo $query_string"
  5. }

We will define a function for initializing the TCL interpreter and parsing the script. This function will be called on startup soon after privileges drop.

Finally we define the request handler invoking the TCL proc and passign args to it

  1. #include <tcl.h>
  2. #include "uwsgi.h"
  3.  
  4. // global interpreter
  5. static Tcl_Interp *tcl_interp;
  6.  
  7. // the init function
  8. void ourtcl_init() {
  9. // create the TCL interpreter
  10. tcl_interp = Tcl_CreateInterp() ;
  11. if (!tcl_interp) {
  12. uwsgi_log("unable to initialize TCL interpreter\n");
  13. exit(1);
  14. }
  15.  
  16. // initialize the interpreter
  17. if (Tcl_Init(tcl_interp) != TCL_OK) {
  18. uwsgi_log("Tcl_Init error: %s\n", Tcl_GetStringResult(tcl_interp));
  19. exit(1);
  20. }
  21.  
  22. // parse foo.tcl
  23. if (Tcl_EvalFile(tcl_interp, "foo.tcl") != TCL_OK) {
  24. uwsgi_log("Tcl_EvalFile error: %s\n", Tcl_GetStringResult(tcl_interp));
  25. exit(1);
  26. }
  27.  
  28. uwsgi_log("TCL engine initialized");
  29. }
  30.  
  31. // the request handler
  32. int ourtcl_handler(struct wsgi_request *wsgi_req) {
  33.  
  34. // get request vars
  35. if (uwsgi_parse_vars(wsgi_req)) return -1;
  36.  
  37. Tcl_Obj *objv[4];
  38. // the proc name
  39. objv[0] = Tcl_NewStringObj("request_handler", -1);
  40. // REMOTE_ADDR
  41. objv[1] = Tcl_NewStringObj(wsgi_req->remote_addr, wsgi_req->remote_addr_len);
  42. // PATH_INFO
  43. objv[2] = Tcl_NewStringObj(wsgi_req->path_info, wsgi_req->path_info_len);
  44. // QUERY_STRING
  45. objv[3] = Tcl_NewStringObj(wsgi_req->query_string, wsgi_req->query_string_len);
  46.  
  47. // call the proc
  48. if (Tcl_EvalObjv(tcl_interp, 4, objv, TCL_EVAL_GLOBAL) != TCL_OK) {
  49. // ERROR, report it to the browser
  50. if (uwsgi_response_prepare_headers(wsgi_req, "500 Internal Server Error", 25)) return -1;
  51. if (uwsgi_response_add_content_type(wsgi_req, "text/plain", 10)) return -1;
  52. char *body = (char *) Tcl_GetStringResult(tcl_interp);
  53. if (uwsgi_response_write_body_do(wsgi_req, body, strlen(body))) return -1;
  54. return UWSGI_OK;
  55. }
  56.  
  57. // all fine
  58. if (uwsgi_response_prepare_headers(wsgi_req, "200 OK", 6)) return -1;
  59. if (uwsgi_response_add_content_type(wsgi_req, "text/plain", 10)) return -1;
  60.  
  61. // write the result
  62. char *body = (char *) Tcl_GetStringResult(tcl_interp);
  63. if (uwsgi_response_write_body_do(wsgi_req, body, strlen(body))) return -1;
  64. return UWSGI_OK;
  65. }

You can build it with:

  1. gcc -fPIC -shared -o ourtcl.so `./uwsgi/uwsgi --cflags` -I/usr/include/tcl ourtcl.c -ltcl

The only differences from the previous example are the -I and -l for adding the TCL headers and library.

So, let’s run it with:

  1. uwsgi --dlopen ./ourtcl.so --hook-as-user call:ourtcl_init --http-socket :9090 --symcall ourtcl_handler --http-socket-modifier1 18

Here the only new player is —hook-as-user call:ourtcl_init invoking the specified function after privileges drop.

Note

This code is not thread safe! If you want to improve this tcl library to support multithreading, best approach will be having a TCL interpreterfor each pthread instead of a global one.

Considerations

Since uWSGI 1.9.21, thanks to the —build-plugin option, developing uWSGI plugins has become really easy.

The symcall plugin is for tiny libraries/pieces of code, for bigger needs consider developing a full plugin.

The tcl example we have seen before is maybe the right example of “wrong” usage ;)