{"id":21970799,"url":"https://github.com/kingluo/lua-resty-ffi","last_synced_at":"2025-04-06T10:10:34.974Z","repository":{"id":63831597,"uuid":"535226549","full_name":"kingluo/lua-resty-ffi","owner":"kingluo","description":"lua-resty-ffi provides an efficient and generic API to do hybrid programming in openresty/envoy with mainstream languages (Go, Python, Java, Rust, Nodejs, etc.).","archived":false,"fork":false,"pushed_at":"2025-01-01T04:15:37.000Z","size":501,"stargazers_count":110,"open_issues_count":1,"forks_count":11,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-30T09:05:46.647Z","etag":null,"topics":["coroutines","envoy","ffi","golang","java","libevent","lua","luajit","luarocks","nginx","nginx-proxy","nodejs","nonblocking","openresty","python3","rust","rust-lang"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kingluo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":null,"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"lfx_crowdfunding":null,"custom":"https://www.buymeacoffee.com/kingluo"}},"created_at":"2022-09-11T07:46:54.000Z","updated_at":"2025-03-17T10:21:19.000Z","dependencies_parsed_at":"2024-01-12T09:12:49.243Z","dependency_job_id":"b6aedddd-2e14-4fa0-9b3b-44ede36c20b4","html_url":"https://github.com/kingluo/lua-resty-ffi","commit_stats":{"total_commits":191,"total_committers":1,"mean_commits":191.0,"dds":0.0,"last_synced_commit":"33b66308a006a8d2461a7186949255a7c5375428"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kingluo%2Flua-resty-ffi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kingluo%2Flua-resty-ffi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kingluo%2Flua-resty-ffi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kingluo%2Flua-resty-ffi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kingluo","download_url":"https://codeload.github.com/kingluo/lua-resty-ffi/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247464220,"owners_count":20942970,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["coroutines","envoy","ffi","golang","java","libevent","lua","luajit","luarocks","nginx","nginx-proxy","nodejs","nonblocking","openresty","python3","rust","rust-lang"],"created_at":"2024-11-29T14:43:10.538Z","updated_at":"2025-04-06T10:10:34.941Z","avatar_url":"https://github.com/kingluo.png","language":"C","funding_links":["https://www.buymeacoffee.com/kingluo"],"categories":["C"],"sub_categories":[],"readme":"# lua-resty-ffi\n\nlua-resty-ffi provides an efficient and generic API to do hybrid programming in openresty with mainstream languages\n([Go](examples/go), [Python](examples/python), [Java](examples/java), [Rust](examples/rust), [Node.js](examples/nodejs), etc.).\n\n**Now it supports [Envoy](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter)!**\n\n**That means all lua-resty-ffi based libraries (e.g. [lua-resty-ffi-grpc](https://github.com/kingluo/lua-resty-ffi-grpc)) can run on both Nginx and Envoy seamlessly without code changes.**\n\n**Features:**\n* nonblocking, in coroutine way\n* simple but extensible interface, supports any C ABI compliant language\n* once and for all, no need to write C/Lua codes to do coupling anymore\n* high performance, [faster](benchmark.md) than unix domain socket way\n* generic loader library for python/java/nodejs\n* any serialization message format you like\n\n**TODO:**\n* [x] envoy porting\n* [ ] batch polling\n* [ ] use spinlock\n* [ ] python: support running venv package based on sub-interpreter\n* [ ] logging to nginx error.log (aware of file rotation), e.g. python [WatchedFileHandler](https://stackoverflow.com/questions/9106795/python-logging-and-rotating-files)\n* [ ] shared_dict API\n\n**Sub-Projects:**\n* Go\n  * [lua-resty-ffi-etcd](https://github.com/kingluo/lua-resty-ffi-etcd)\n  * [lua-resty-ffi-kafka](https://github.com/kingluo/lua-resty-ffi-kafka)\n  * [lua-resty-ffi-req](https://github.com/kingluo/lua-resty-ffi-req) HTTP 1/2/3 client\n* Python\n  * [lua-resty-ffi-ldap](https://github.com/kingluo/lua-resty-ffi-ldap) ldap client, supports all SASL auth methods\n  * [lua-resty-ffi-graphql-resolver](https://github.com/kingluo/lua-resty-ffi-graphql-resolver) embed graphql server into nginx\n  * [lua-resty-ffi-soap](https://github.com/kingluo/lua-resty-ffi-soap) The openresty SOAP to REST library based on zeep.\n* Rust\n  * [lua-resty-ffi-grpc](https://github.com/kingluo/lua-resty-ffi-grpc) grpc client based on tonic\n\n## Architecture\n\n![architecture](architecture.png)\n\n![callflow](lua-resty-ffi-callflow.svg)\n\nUseful blog post:\n\n[Implement Grpc Client in Rust for Openresty](http://luajit.io/post/implement-grpc-client-in-rust-for-openresty/) ([Chinese version](https://zhuanlan.zhihu.com/p/586934870))\n\n## Quickstart\n\n### Install lua-resty-ffi via luarocks\n\n* specify your openresty source path in variable `$OR_SRC`\n* ensure openresty source are already configured and built according to your product release\n\n```bash\nluarocks config variables.OR_SRC /tmp/tmp.Z2UhJbO1Si/openresty-1.21.4.1\nluarocks install lua-resty-ffi\n```\n\n### Demo\n\nTake golang as an example:\n\n```bash\ncd examples/go\n\n# install golang if not yet, see https://go.dev/doc/install\n# compile example libraries\nmake\n\n# run nginx\nmake run\n\n# in another terminal\ncurl http://localhost:20000/echo\nok\n```\n\n### envoy\n\nBlog:\n\nhttp://luajit.io/posts/make-lua-resty-ffi-run-on-envoy/\n\nhttp://luajit.io/posts/envoy-async-http-filter-lua-resty-ffi-vs-golang/\n\nRepo:\n\nhttps://github.com/kingluo/envoy/tree/lua-resty-ffi\n\n```bash\n# compile envoy\ncd /opt\ngit clone https://github.com/kingluo/envoy\ncd envoy\ngit checkout origin/lua-resty-ffi\n\n./ci/run_envoy_docker.sh './ci/do_ci.sh bazel.dev'\n\nln -f /tmp/envoy-docker-build/envoy/source/exe/envoy/envoy /usr/local/bin/envoy\n\n# run demo\ncd /opt/envoy/examples/lua\nPYTHONPATH=/opt/lua-resty-ffi/examples/python/ \\\nLD_LIBRARY_PATH=/opt/lua-resty-ffi/examples/python \\\nLUA_PATH='/opt/lua-resty-ffi/?.lua;;' \\\nenvoy -c envoy.yaml --concurrency 1\n\ncurl -v localhost:8000/httpbin/get\n```\n\n**envoy.yaml snippet:**\n\n```yaml\n  http_filters:\n  - name: lua_filter_with_custom_name_0\n    typed_config:\n      \"@type\": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua\n      default_source_code:\n        inline_string: |\n          function envoy_on_request(request_handle)\n            require(\"resty_ffi\")\n            local demo = ngx.load_ffi(\"ffi_go_echo\")\n            local ok, res = demo:foobar(\"foobar\", request_handle)\n            assert(ok)\n            assert(res == \"foobar\")\n            local demo = ngx.load_ffi(\"resty_ffi_python\", \"ffi.echo?,init\", {is_global = true})\n            local ok, res = demo:echo(\"hello\", request_handle)\n            assert(ok)\n            assert(res == \"hello\")\n          end\n```\n\n## Background\n\nIn openresty land, when you turn to implement some logic, especially to couple with third-party popular frameworks, it's likely to suck in awkward: make bricks without straw.\n\n1. C is a low-level language, with no unified and rich libraries and ecosystems, and most modern frameworks do not support C, instead, they like Java, Python, Go. C is suitable for fundamental software.\n\n2. Lua is an embedded and minimal programming language, which means all powers come from the host. In openresty, it means all functionalities come from lua-nginx-modules. Like C, or even worse, you have to reinvent the wheels via cosocket to do modern networking stuff. A lot of `lua-resty-*` was born, but they are almost semi-finished compared to native lib in other languages. For example, `lua-resty-kafka` doesn't support consumer groups, `lua-resty-postgres` doesn't support notify and prepared statements, etc. Moreover, most of those authors of `lua-resty-*` stop development at some stage because the lua community is so small and less attractive.\n\n**Why not WASM?**\n\nWASM has below shortages, which make it not suitable for openresty:\n\n* no coroutine, which means you need to execute the logic from start to end and block the nginx worker process with arbitrary time\n* castrated language support, e.g.\n  * Go: You have to use tinygo instead, not the batteries-included official golang.\n  * Rust: You have to use specialized crates to do jobs, e.g. when you need async network programming,\n[tokio](https://tokio.rs/) is unavailable, instead, you have to use WASI-based crates, e.g. [`wasmedge_wasi_socket`](https://wasmedge.org/book/en/write_wasm/rust/networking-nonblocking.html).\n  * Python: You have to use implementations that support WASM, e.g. rustpython.\n* complex development, due to sandbox original intention, you have to export a lot of API from nginx\n\n**So, may I extend the openresty with modern programming languages (Go, Python, Java, Rust, etc.)\nand reuse their rich ecosystems directly? Yes, that's what lua-resty-ffi does.**\n\n## Concepts\n\n### Library\n\nIn Go and Rust, it means the compiled library, e.g. `libffi_go_etcd.so`.\n\nIn Python3, it means the loader library `libffi_python3.so` with native python3 modules.\n\nIn Java, it means the loader library `libffi_java.so` with native Java classes/jar.\n\nIn Node.js, it means the loader library `libffi_nodejs.so` with native nodejs modules.\n\n### Library configuration\n\nConfiguration of the library, e.g. etcd endpoints, kafka endpoints, etc.\n\nThe format could be any serialization format, e.g. json, yaml, as long as it matches the runtime \n\n### Runtime\n\nThe combination of library and configuration would init a new runtime,\nwhich represents some threads or goroutines to do jobs.\n\nYou could use the same library with different configurations, which is very common,\nespecially for Java, Python and Node.js.\n\n### Request-Response Model\n\nCoupling between nginx worker process and the runtime is based on message exchanges, which contain two directions:\n\n1. **Request**\n\n* the lua coroutine creates a task\n* associates the task with the request message, which is C `malloc()` char array. Note that\nthis char array would be freed by lua-resty-ffi, and the runtime just uses it.\n* put the task into the thread-safe queue of the runtime and yield\n* the runtime polls this queue\n\nWhy not call API provided by other languages?\n* In Go, due to GMP model, it may block the nginx worker process\n* It increases the burden for other languages to provide such API\n\n2. **Response**\n\nThe runtime injects the response (also C `malloc()` char array)\ninto the `ngx_thread_pool_done` queue [**directly**](https://github.com/kingluo/lua-resty-ffi/blob/main/patches/ngx_thread_pool.c.patch) and notify the nginx epoll loop via eventfd,\nthe nginx would resume the lua coroutine then. Note that the response would be\nfreed by lua-resty-ffi, so no need to care about it in your runtime.\n\n## IPC design and Benchmark\n\n[Benchmark compared to unix domain socket.](benchmark.md)\n\n## Lua API\n\n### `local runtime = ngx.load_ffi(lib, cfg, opts)`\n\nLoad and return the runtime\n\n* `lib`\nshared library name. It could be an absolute file path or name only,\nor even a short name (e.g. for `libdemo.so`, the short name is `demo`).\nWhen the `lib` is name only or short name, it's searched according to `LD_LIBRARY_PATH` environment variable.\n\n* `cfg` configuration, it could be string or nil.\n\n* `opts` options table.\n\n```lua\n{\n    -- the maximum queue size for pending requests to the runtime.\n    -- it determines the throughput of requests if the queue is full,\n    -- all following requests would fail.\n    max_queue = 65536,\n\n    -- denotes whether the symbols loaded from the library\n    -- would be exported in the global namespace, which is only necessary for python3 and nodejs.\n    is_global = false,\n\n    -- by default, all libraries handles would be cached by lua-resty-ffi\n    -- because currently, only python and rust could be hot-reload,\n    -- and java must not be `dlclose()`\n    -- unpin is used to enable hot-reload\n    -- note that it's different from the unload/reload of runtime,\n    -- which is application-specific behavior, but library unload/reload is\n    -- done by the linker via dlopen()/dlclose().\n    unpin = false,\n}\n```\n\nThis API is idempotent. The loaded runtime is cached in an internal table, where\nthe table key is `lib .. '\u0026' .. cfg`.\n\nThis function calls the `libffi_init()` of the library per key.\n\nIt means the same library with a different configuration would initiate a different new runtime,\nwhich is especially useful for python3, Java and Node.js.\n\nExample:\n\n```lua\nlocal opts = {is_global = true}\nlocal demo = ngx.load_ffi(\"ffi_python3\",\n    [[ffi.kafka,init,{\"servers\":\"localhost:9092\", \"topic\":\"foobar\", \"group_id\": \"foobar\"}]], opts)\n\nlocal demo = ngx.load_ffi(\"ffi_go_etcd\", \"[\\\"localhost:2379\\\"]\")\n```\n\n### `local ok, res_or_rc, err = runtime:call(req)`\n\nSend a request to the runtime and returns the response.\n\n* `req` the request string, could be in any serialization format, e.g. json, protobuf, as long as it matches the runtime implementation.\n\n* `ok` return status, true or false.\n\n* `res_or_rc` response string, could be in any serialization format, e.g. json, protobuf, as long as it matches the runtime implementation. When the runtime returns non-zero `rc`, `ok` is false, and the `res_or_rc` is the returned value by the runtime.\n\n* `err` the error string, it may exist only if `ok` is false. It may be nil if the runtime does not return an error.\n\nThis method is nonblocking, which means the coroutine would yield waiting for the response and resume with the return values.\n\nNote that the method name `call` could be any name you like, it would be generated automatically by the `__index` meta function, and only used to denote the request semantics。\n\nExample:\n\n```lua\nlocal ok, res\nok, res = demo:produce([[{\"type\":\"produce\", \"msg\":\"hello\"}]])\nassert(ok)\nok, res = demo:produce([[{\"type\":\"produce\", \"msg\":\"world\"}]])\nassert(ok)\nngx.sleep(2)\nok, res = demo:consume([[{\"type\":\"consume\"}]])\nassert(ok)\nngx.say(res)\n```\n\n### `local ok, res = runtime:__unload()`\n\nUnload the runtime, after that, no request could be sent to this runtime anymore.\nThe runtime would receive a NULL task, and it must terminate everything including the threads.\nNote that it's asynchronous processing, and the NULL task is appended to the queue, so\nall pending normal tasks would be handled first.\n\n## API provided by the runtime\n\n### `int libffi_init(char* cfg, void *tq);`\n\nThis API is provided by the library to initiate its logic and start the poll thread/goroutine.\n\n`cfg` is a null-terminated C string, it would get freed by lua-resty-ffi\nafter `libffi_init()` returns, which may be `NULL`.\n\n`tq` is the task queue pointer, used by the below APIs.\n\nExample:\n\n```go\n//export libffi_init\nfunc libffi_init(cfg *C.char, tq unsafe.Pointer) C.int {\n    data := C.GoString(cfg)\n    ...\n    go func() {\n        for {\n            task := C.ngx_http_lua_ffi_task_poll(tq)\n            if task == nil {\n                break\n            }\n            var rlen C.int\n            r := C.ngx_http_lua_ffi_get_req(task, \u0026rlen)\n            res := C.malloc(C.ulong(rlen))\n            C.memcpy(res, unsafe.Pointer(r), C.ulong(rlen))\n            C.ngx_http_lua_ffi_respond(task, 0, (*C.char)(res), rlen)\n        }\n    }()\n    return 0\n}\n```\n\n## APIs used by the runtime\n\n### `void* ngx_http_lua_ffi_task_poll(void *tq);`\n\nPoll the task from the task queue assigned to the runtime.\n\nWhen it returns `NULL`, it denotes the runtime was unloaded, the runtime must clean up\neverything and not access the task queue anymore (because the task queue was deallocated)!\n\n### `char* ngx_http_lua_ffi_get_req(void *tsk, int *len);`\n\nExtract the request from the task. Note that the request could be NULL, so the runtime must not use this API\nin this case.\n\n### `void ngx_http_lua_ffi_respond(void *tsk, int rc, char* rsp, int rsp_len);`\n\nResponse to the task.\n\nAll the above APIs are thread-safe. So you could use them anywhere in the thread/goroutine of your runtime.\n\n* `rc` return status, `0` means successful, and other values mean failure.\n* `rsp` response char array, may be NULL if the runtime does not need to respond to something.\n* `rsp_len` the length of response, maybe `0` if the `rsp` is NULL or `\\0' terminated C string.\n\nIf `rc` is non-zero, then the runtime may also set `rsp` and `rsp_len` if it needs to return error data.\n\n## Code hot-reload\n\n[How to do code hot-reload in lua-resty-ffi?](https://github.com/kingluo/lua-resty-ffi/blob/main/hot-reload.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkingluo%2Flua-resty-ffi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkingluo%2Flua-resty-ffi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkingluo%2Flua-resty-ffi/lists"}