├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── Makefile ├── Makefile-python.rocks ├── Makefile.rocks ├── README.md ├── architecture.png ├── benchmark.md ├── benchmark1.png ├── benchmark2.png ├── build-id.c ├── build-id.h ├── build_shared.sh ├── examples ├── go │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── benchmark.lua │ ├── echo.go │ ├── go.mod │ ├── go.sum │ ├── nginx.conf │ └── unix_socket_server.go ├── java │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── build.sh │ ├── echo-demo │ │ ├── .gitignore │ │ ├── .mvn │ │ │ └── wrapper │ │ │ │ ├── maven-wrapper.jar │ │ │ │ └── maven-wrapper.properties │ │ ├── mvnw │ │ ├── pom.xml │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── demo │ │ │ └── echo │ │ │ └── App.java │ ├── http2-demo │ │ ├── .gitignore │ │ ├── .mvn │ │ │ └── wrapper │ │ │ │ ├── maven-wrapper.jar │ │ │ │ └── maven-wrapper.properties │ │ ├── mvnw │ │ ├── pom.xml │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── demo │ │ │ └── http2 │ │ │ └── App.java │ ├── loader.c │ ├── loader │ │ ├── .gitignore │ │ ├── .mvn │ │ │ └── wrapper │ │ │ │ ├── maven-wrapper.jar │ │ │ │ └── maven-wrapper.properties │ │ ├── mvnw │ │ ├── pom.xml │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── resty │ │ │ └── ffi │ │ │ └── Loader.java │ ├── nginx.conf │ └── run.sh ├── nodejs │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── binding.gyp │ ├── build.sh │ ├── demo │ │ ├── echo.js │ │ └── http2.js │ ├── loader.cc │ ├── nginx.conf │ └── resty_ffi.cc ├── python │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── build.sh │ ├── docker-compose.yml │ ├── ffi │ │ ├── echo.py │ │ └── kafka.py │ ├── loader.c │ ├── nginx.conf │ └── requirements.txt └── rust │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── echo │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── lib.rs │ └── nginx.conf ├── hot-reload.md ├── lua-resty-ffi-callflow.svg ├── ngx_http_lua_ffi.c ├── resty_ffi.lua └── rockspec ├── lua-resty-ffi-main-1.rockspec ├── lua-resty-ffi-main-2.rockspec ├── lua-resty-ffi-python-main-1.rockspec └── lua-resty-ffi-python-main-2.rockspec /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: https://www.buymeacoffee.com/kingluo 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.so 2 | symbols.h 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Jinhua Luo (kingluo) luajit.io@gmail.com 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := libresty_ffi.so 2 | 3 | include $(NGX_OBJS_MAKEFILE) 4 | 5 | libresty_ffi.so: 6 | $(CC) $(CFLAGS) $(ALL_INCS) -I$(NGX_LUA_SRC) -ggdb3 -shared -fPIC -DSHARED_OBJECT -ldl \ 7 | $(SRC)/build-id.c $(SRC)/ngx_http_lua_ffi.c -o $(SRC)/$@ 8 | -------------------------------------------------------------------------------- /Makefile-python.rocks: -------------------------------------------------------------------------------- 1 | INST_PREFIX ?= /usr 2 | INST_LIBDIR ?= $(INST_PREFIX)/lib/lua/5.1 3 | INSTALL ?= install 4 | 5 | .PHONY: build 6 | build: 7 | cd examples/python; ./build.sh 8 | 9 | .PHONY: install 10 | install: 11 | $(INSTALL) -d $(INST_LIBDIR)/ 12 | $(INSTALL) examples/python/libresty_ffi_python.so $(INST_LIBDIR)/ 13 | -------------------------------------------------------------------------------- /Makefile.rocks: -------------------------------------------------------------------------------- 1 | INST_PREFIX ?= /usr 2 | INST_LIBDIR ?= $(INST_PREFIX)/lib/lua/5.1 3 | INST_LUADIR ?= $(INST_PREFIX)/share/lua/5.1 4 | INSTALL ?= install 5 | 6 | .PHONY: build 7 | build: 8 | ./build_shared.sh 9 | 10 | .PHONY: install 11 | install: 12 | $(INSTALL) -d $(INST_LUADIR)/ 13 | $(INSTALL) resty_ffi.lua $(INST_LUADIR)/ 14 | $(INSTALL) -d $(INST_LIBDIR)/ 15 | $(INSTALL) libresty_ffi.so $(INST_LIBDIR)/ 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-resty-ffi 2 | 3 | lua-resty-ffi provides an efficient and generic API to do hybrid programming in openresty with mainstream languages 4 | ([Go](examples/go), [Python](examples/python), [Java](examples/java), [Rust](examples/rust), [Node.js](examples/nodejs), etc.). 5 | 6 | **Now it supports [Envoy](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter)!** 7 | 8 | **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.** 9 | 10 | **Features:** 11 | * nonblocking, in coroutine way 12 | * simple but extensible interface, supports any C ABI compliant language 13 | * once and for all, no need to write C/Lua codes to do coupling anymore 14 | * high performance, [faster](benchmark.md) than unix domain socket way 15 | * generic loader library for python/java/nodejs 16 | * any serialization message format you like 17 | 18 | **TODO:** 19 | * [x] envoy porting 20 | * [ ] batch polling 21 | * [ ] use spinlock 22 | * [ ] python: support running venv package based on sub-interpreter 23 | * [ ] logging to nginx error.log (aware of file rotation), e.g. python [WatchedFileHandler](https://stackoverflow.com/questions/9106795/python-logging-and-rotating-files) 24 | * [ ] shared_dict API 25 | 26 | **Sub-Projects:** 27 | * Go 28 | * [lua-resty-ffi-etcd](https://github.com/kingluo/lua-resty-ffi-etcd) 29 | * [lua-resty-ffi-kafka](https://github.com/kingluo/lua-resty-ffi-kafka) 30 | * [lua-resty-ffi-req](https://github.com/kingluo/lua-resty-ffi-req) HTTP 1/2/3 client 31 | * Python 32 | * [lua-resty-ffi-ldap](https://github.com/kingluo/lua-resty-ffi-ldap) ldap client, supports all SASL auth methods 33 | * [lua-resty-ffi-graphql-resolver](https://github.com/kingluo/lua-resty-ffi-graphql-resolver) embed graphql server into nginx 34 | * [lua-resty-ffi-soap](https://github.com/kingluo/lua-resty-ffi-soap) The openresty SOAP to REST library based on zeep. 35 | * Rust 36 | * [lua-resty-ffi-grpc](https://github.com/kingluo/lua-resty-ffi-grpc) grpc client based on tonic 37 | 38 | ## Architecture 39 | 40 | ![architecture](architecture.png) 41 | 42 | ![callflow](lua-resty-ffi-callflow.svg) 43 | 44 | Useful blog post: 45 | 46 | [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)) 47 | 48 | ## Quickstart 49 | 50 | ### Install lua-resty-ffi via luarocks 51 | 52 | * specify your openresty source path in variable `$OR_SRC` 53 | * ensure openresty source are already configured and built according to your product release 54 | 55 | ```bash 56 | luarocks config variables.OR_SRC /tmp/tmp.Z2UhJbO1Si/openresty-1.21.4.1 57 | luarocks install lua-resty-ffi 58 | ``` 59 | 60 | ### Demo 61 | 62 | Take golang as an example: 63 | 64 | ```bash 65 | cd examples/go 66 | 67 | # install golang if not yet, see https://go.dev/doc/install 68 | # compile example libraries 69 | make 70 | 71 | # run nginx 72 | make run 73 | 74 | # in another terminal 75 | curl http://localhost:20000/echo 76 | ok 77 | ``` 78 | 79 | ### envoy 80 | 81 | Blog: 82 | 83 | http://luajit.io/posts/make-lua-resty-ffi-run-on-envoy/ 84 | 85 | http://luajit.io/posts/envoy-async-http-filter-lua-resty-ffi-vs-golang/ 86 | 87 | Repo: 88 | 89 | https://github.com/kingluo/envoy/tree/lua-resty-ffi 90 | 91 | ```bash 92 | # compile envoy 93 | cd /opt 94 | git clone https://github.com/kingluo/envoy 95 | cd envoy 96 | git checkout origin/lua-resty-ffi 97 | 98 | ./ci/run_envoy_docker.sh './ci/do_ci.sh bazel.dev' 99 | 100 | ln -f /tmp/envoy-docker-build/envoy/source/exe/envoy/envoy /usr/local/bin/envoy 101 | 102 | # run demo 103 | cd /opt/envoy/examples/lua 104 | PYTHONPATH=/opt/lua-resty-ffi/examples/python/ \ 105 | LD_LIBRARY_PATH=/opt/lua-resty-ffi/examples/python \ 106 | LUA_PATH='/opt/lua-resty-ffi/?.lua;;' \ 107 | envoy -c envoy.yaml --concurrency 1 108 | 109 | curl -v localhost:8000/httpbin/get 110 | ``` 111 | 112 | **envoy.yaml snippet:** 113 | 114 | ```yaml 115 | http_filters: 116 | - name: lua_filter_with_custom_name_0 117 | typed_config: 118 | "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua 119 | default_source_code: 120 | inline_string: | 121 | function envoy_on_request(request_handle) 122 | require("resty_ffi") 123 | local demo = ngx.load_ffi("ffi_go_echo") 124 | local ok, res = demo:foobar("foobar", request_handle) 125 | assert(ok) 126 | assert(res == "foobar") 127 | local demo = ngx.load_ffi("resty_ffi_python", "ffi.echo?,init", {is_global = true}) 128 | local ok, res = demo:echo("hello", request_handle) 129 | assert(ok) 130 | assert(res == "hello") 131 | end 132 | ``` 133 | 134 | ## Background 135 | 136 | In 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. 137 | 138 | 1. 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. 139 | 140 | 2. 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. 141 | 142 | **Why not WASM?** 143 | 144 | WASM has below shortages, which make it not suitable for openresty: 145 | 146 | * no coroutine, which means you need to execute the logic from start to end and block the nginx worker process with arbitrary time 147 | * castrated language support, e.g. 148 | * Go: You have to use tinygo instead, not the batteries-included official golang. 149 | * Rust: You have to use specialized crates to do jobs, e.g. when you need async network programming, 150 | [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). 151 | * Python: You have to use implementations that support WASM, e.g. rustpython. 152 | * complex development, due to sandbox original intention, you have to export a lot of API from nginx 153 | 154 | **So, may I extend the openresty with modern programming languages (Go, Python, Java, Rust, etc.) 155 | and reuse their rich ecosystems directly? Yes, that's what lua-resty-ffi does.** 156 | 157 | ## Concepts 158 | 159 | ### Library 160 | 161 | In Go and Rust, it means the compiled library, e.g. `libffi_go_etcd.so`. 162 | 163 | In Python3, it means the loader library `libffi_python3.so` with native python3 modules. 164 | 165 | In Java, it means the loader library `libffi_java.so` with native Java classes/jar. 166 | 167 | In Node.js, it means the loader library `libffi_nodejs.so` with native nodejs modules. 168 | 169 | ### Library configuration 170 | 171 | Configuration of the library, e.g. etcd endpoints, kafka endpoints, etc. 172 | 173 | The format could be any serialization format, e.g. json, yaml, as long as it matches the runtime 174 | 175 | ### Runtime 176 | 177 | The combination of library and configuration would init a new runtime, 178 | which represents some threads or goroutines to do jobs. 179 | 180 | You could use the same library with different configurations, which is very common, 181 | especially for Java, Python and Node.js. 182 | 183 | ### Request-Response Model 184 | 185 | Coupling between nginx worker process and the runtime is based on message exchanges, which contain two directions: 186 | 187 | 1. **Request** 188 | 189 | * the lua coroutine creates a task 190 | * associates the task with the request message, which is C `malloc()` char array. Note that 191 | this char array would be freed by lua-resty-ffi, and the runtime just uses it. 192 | * put the task into the thread-safe queue of the runtime and yield 193 | * the runtime polls this queue 194 | 195 | Why not call API provided by other languages? 196 | * In Go, due to GMP model, it may block the nginx worker process 197 | * It increases the burden for other languages to provide such API 198 | 199 | 2. **Response** 200 | 201 | The runtime injects the response (also C `malloc()` char array) 202 | into 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, 203 | the nginx would resume the lua coroutine then. Note that the response would be 204 | freed by lua-resty-ffi, so no need to care about it in your runtime. 205 | 206 | ## IPC design and Benchmark 207 | 208 | [Benchmark compared to unix domain socket.](benchmark.md) 209 | 210 | ## Lua API 211 | 212 | ### `local runtime = ngx.load_ffi(lib, cfg, opts)` 213 | 214 | Load and return the runtime 215 | 216 | * `lib` 217 | shared library name. It could be an absolute file path or name only, 218 | or even a short name (e.g. for `libdemo.so`, the short name is `demo`). 219 | When the `lib` is name only or short name, it's searched according to `LD_LIBRARY_PATH` environment variable. 220 | 221 | * `cfg` configuration, it could be string or nil. 222 | 223 | * `opts` options table. 224 | 225 | ```lua 226 | { 227 | -- the maximum queue size for pending requests to the runtime. 228 | -- it determines the throughput of requests if the queue is full, 229 | -- all following requests would fail. 230 | max_queue = 65536, 231 | 232 | -- denotes whether the symbols loaded from the library 233 | -- would be exported in the global namespace, which is only necessary for python3 and nodejs. 234 | is_global = false, 235 | 236 | -- by default, all libraries handles would be cached by lua-resty-ffi 237 | -- because currently, only python and rust could be hot-reload, 238 | -- and java must not be `dlclose()` 239 | -- unpin is used to enable hot-reload 240 | -- note that it's different from the unload/reload of runtime, 241 | -- which is application-specific behavior, but library unload/reload is 242 | -- done by the linker via dlopen()/dlclose(). 243 | unpin = false, 244 | } 245 | ``` 246 | 247 | This API is idempotent. The loaded runtime is cached in an internal table, where 248 | the table key is `lib .. '&' .. cfg`. 249 | 250 | This function calls the `libffi_init()` of the library per key. 251 | 252 | It means the same library with a different configuration would initiate a different new runtime, 253 | which is especially useful for python3, Java and Node.js. 254 | 255 | Example: 256 | 257 | ```lua 258 | local opts = {is_global = true} 259 | local demo = ngx.load_ffi("ffi_python3", 260 | [[ffi.kafka,init,{"servers":"localhost:9092", "topic":"foobar", "group_id": "foobar"}]], opts) 261 | 262 | local demo = ngx.load_ffi("ffi_go_etcd", "[\"localhost:2379\"]") 263 | ``` 264 | 265 | ### `local ok, res_or_rc, err = runtime:call(req)` 266 | 267 | Send a request to the runtime and returns the response. 268 | 269 | * `req` the request string, could be in any serialization format, e.g. json, protobuf, as long as it matches the runtime implementation. 270 | 271 | * `ok` return status, true or false. 272 | 273 | * `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. 274 | 275 | * `err` the error string, it may exist only if `ok` is false. It may be nil if the runtime does not return an error. 276 | 277 | This method is nonblocking, which means the coroutine would yield waiting for the response and resume with the return values. 278 | 279 | Note 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。 280 | 281 | Example: 282 | 283 | ```lua 284 | local ok, res 285 | ok, res = demo:produce([[{"type":"produce", "msg":"hello"}]]) 286 | assert(ok) 287 | ok, res = demo:produce([[{"type":"produce", "msg":"world"}]]) 288 | assert(ok) 289 | ngx.sleep(2) 290 | ok, res = demo:consume([[{"type":"consume"}]]) 291 | assert(ok) 292 | ngx.say(res) 293 | ``` 294 | 295 | ### `local ok, res = runtime:__unload()` 296 | 297 | Unload the runtime, after that, no request could be sent to this runtime anymore. 298 | The runtime would receive a NULL task, and it must terminate everything including the threads. 299 | Note that it's asynchronous processing, and the NULL task is appended to the queue, so 300 | all pending normal tasks would be handled first. 301 | 302 | ## API provided by the runtime 303 | 304 | ### `int libffi_init(char* cfg, void *tq);` 305 | 306 | This API is provided by the library to initiate its logic and start the poll thread/goroutine. 307 | 308 | `cfg` is a null-terminated C string, it would get freed by lua-resty-ffi 309 | after `libffi_init()` returns, which may be `NULL`. 310 | 311 | `tq` is the task queue pointer, used by the below APIs. 312 | 313 | Example: 314 | 315 | ```go 316 | //export libffi_init 317 | func libffi_init(cfg *C.char, tq unsafe.Pointer) C.int { 318 | data := C.GoString(cfg) 319 | ... 320 | go func() { 321 | for { 322 | task := C.ngx_http_lua_ffi_task_poll(tq) 323 | if task == nil { 324 | break 325 | } 326 | var rlen C.int 327 | r := C.ngx_http_lua_ffi_get_req(task, &rlen) 328 | res := C.malloc(C.ulong(rlen)) 329 | C.memcpy(res, unsafe.Pointer(r), C.ulong(rlen)) 330 | C.ngx_http_lua_ffi_respond(task, 0, (*C.char)(res), rlen) 331 | } 332 | }() 333 | return 0 334 | } 335 | ``` 336 | 337 | ## APIs used by the runtime 338 | 339 | ### `void* ngx_http_lua_ffi_task_poll(void *tq);` 340 | 341 | Poll the task from the task queue assigned to the runtime. 342 | 343 | When it returns `NULL`, it denotes the runtime was unloaded, the runtime must clean up 344 | everything and not access the task queue anymore (because the task queue was deallocated)! 345 | 346 | ### `char* ngx_http_lua_ffi_get_req(void *tsk, int *len);` 347 | 348 | Extract the request from the task. Note that the request could be NULL, so the runtime must not use this API 349 | in this case. 350 | 351 | ### `void ngx_http_lua_ffi_respond(void *tsk, int rc, char* rsp, int rsp_len);` 352 | 353 | Response to the task. 354 | 355 | All the above APIs are thread-safe. So you could use them anywhere in the thread/goroutine of your runtime. 356 | 357 | * `rc` return status, `0` means successful, and other values mean failure. 358 | * `rsp` response char array, may be NULL if the runtime does not need to respond to something. 359 | * `rsp_len` the length of response, maybe `0` if the `rsp` is NULL or `\0' terminated C string. 360 | 361 | If `rc` is non-zero, then the runtime may also set `rsp` and `rsp_len` if it needs to return error data. 362 | 363 | ## Code hot-reload 364 | 365 | [How to do code hot-reload in lua-resty-ffi?](https://github.com/kingluo/lua-resty-ffi/blob/main/hot-reload.md) 366 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingluo/lua-resty-ffi/33b66308a006a8d2461a7186949255a7c5375428/architecture.png -------------------------------------------------------------------------------- /benchmark.md: -------------------------------------------------------------------------------- 1 | lua-resty-ffi has a high performance IPC mechanism, based on request-response model. 2 | 3 | Let's check what happens in two directions of such IPC. 4 | 5 | ## request 6 | 7 | The request from lua is appended to a queue. This queue is protected by pthread mutex. 8 | The language runtime polls this queue, if it's empty, then wait on the queue via `pthread_cond_wait()`. 9 | In busy scenario, almost all enqueue/dequeue happens in userspace, without `futex` system calls. 10 | 11 | ## response 12 | 13 | lua-resty-ffi makes full use of nginx nonblocking event loop to dispatch the response from the language runtime. 14 | The response would be injected into the global done queue of nginx, and notify the nginx main thread via eventfd 15 | to handle it. In main thread, the response handler would setup the return value and resume the coroutine 16 | waiting on the response. 17 | 18 | Note that not like [`ngx.run_worker_thread()`](https://github.com/openresty/lua-nginx-module#ngxrun_worker_thread) 19 | (which is also my work contributed to openresty), I do not use nginx thread pool to wait for the response from language runtime. 20 | Instead, we inject it into the global queue of the main thread, which is really a shortcut to boost the performance! 21 | 22 | As known, linux eventfd is high performance. It's just an accumulated counter, so multiple responses 23 | would be folded into one event. 24 | 25 | Both request and response data are exchanged in userspace. 26 | 27 | ## unix domain socket 28 | 29 | In legacy way, we would establish a dedicated process to handle requests from nginx. That's known as proxy model. 30 | 31 | However, unix domain socket is low efficient than mutex/eventfd way, becuase all data are exchanged in kernel space, 32 | involving many system calls and context switches, and moreover, the user program need to prefix the data with length, 33 | because every packet is streaming in the socket. 34 | 35 | ## benchmark 36 | 37 | Let's verify it via the echo example. 38 | 39 | [benchmark.lua](examples/go/benchmark.lua) 40 | 41 | * echo in lua-resty-ffi way: 42 | 43 | ```lua 44 | local demo = ngx.load_ffi("ffi_go_echo") 45 | ngx.update_time() 46 | local t1 = ngx.now() 47 | for _ = 1,n do 48 | -- the echo call is very simple, just send and handle the return value 49 | -- no need to prefix the data with length indicator. 50 | -- both request and response are string, with any format you prefer to use. 51 | -- in this example, they are plain C string. 52 | local ok, res = demo:echo(buf) 53 | assert(ok) 54 | assert(res==buf) 55 | end 56 | ngx.update_time() 57 | local t2 = ngx.now() 58 | ngx.say("ffi: ", t2-t1) 59 | ngx.flush(true) 60 | ``` 61 | 62 | * echo in unix domain socket way: 63 | 64 | ```lua 65 | -- we use cosocket to access unix socket server 66 | local sock = ngx.socket.tcp() 67 | local ok, err = sock:connect("unix:/tmp/echo.sock") 68 | assert(ok) 69 | 70 | local tbuf = {} 71 | local hdr_buf = ffi.new("unsigned char[4]") 72 | 73 | ngx.update_time() 74 | local t1 = ngx.now() 75 | for _ = 1,n do 76 | -- see, we need to encode the request length 77 | local len = buflen 78 | for i = 3, 0, -1 do 79 | hdr_buf[i] = band(len, 255) 80 | len = rshift(len, 8) 81 | end 82 | 83 | tbuf[1] = ffi_str(hdr_buf, 4) 84 | tbuf[2] = buf 85 | local sent = sock:send(tbuf) 86 | assert(sent==buflen+4) 87 | 88 | -- and, we need to receive the response in two parts: 89 | -- first, we need to read the length and decode it, 90 | -- second, we turn to receive the real response data. 91 | -- In openresty, cosocket doesn't have read-any-avaliable 92 | -- semantic, so we have to use two steps here. 93 | local hdr = sock:receive(4) 94 | local hi, mi, li, lli = str_byte(hdr, 1, 4) 95 | local len = 256 * (256 * (256 * hi + mi) + li) + lli 96 | assert(len==buflen) 97 | local data = sock:receive(len) 98 | assert(data==buf) 99 | end 100 | ngx.update_time() 101 | local t2 = ngx.now() 102 | ngx.say("uds: ", t2-t1) 103 | sock:close() 104 | ngx.flush(true) 105 | ``` 106 | 107 | ### benchmark result 108 | 109 | I use AWS EC2 t2.medium (Ubuntu 22.04) to do the benchmark. 110 | 111 | ```bash 112 | # in terminal-1 113 | cd examples/go 114 | make run 115 | 116 | # in terminal-2 117 | cd examples/go 118 | go run unix_socket_server.go 119 | 120 | # in terminal-3 121 | curl 'localhost:20000/benchmark?times=100000&arr=512,4096,10240,102400' 122 | ``` 123 | 124 | ![One CPU Benchmark](benchmark1.png) 125 | 126 | ![Two CPU Benchmark](benchmark2.png) 127 | 128 | Here I use `taskset` to simulate one CPU scenario. 129 | Send 100,000 requests, in lenght of 512B, 4096B, 10KB, 100KB perspectively. 130 | The result is in seconds, lower is better. 131 | 132 | You could see that lua-resty-ffi is faster than unix domain socket, and the difference 133 | is proportional to the length. 134 | -------------------------------------------------------------------------------- /benchmark1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingluo/lua-resty-ffi/33b66308a006a8d2461a7186949255a7c5375428/benchmark1.png -------------------------------------------------------------------------------- /benchmark2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingluo/lua-resty-ffi/33b66308a006a8d2461a7186949255a7c5375428/benchmark2.png -------------------------------------------------------------------------------- /build-id.c: -------------------------------------------------------------------------------- 1 | /* vim: set expandtab tabstop=4 softtabstop=4 shiftwidth=4: */ 2 | /* 3 | * Copyright © 2016 Intel Corporation 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the "Software"), 7 | * to deal in the Software without restriction, including without limitation 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | * and/or sell copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice (including the next 13 | * paragraph) shall be included in all copies or substantial portions of the 14 | * Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | * IN THE SOFTWARE. 23 | */ 24 | 25 | #define _GNU_SOURCE 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include "build-id.h" 32 | 33 | #ifndef NT_GNU_BUILD_ID 34 | #define NT_GNU_BUILD_ID 3 35 | #endif 36 | 37 | #ifndef ElfW 38 | #define ElfW(type) Elf_##type 39 | #endif 40 | 41 | #define ALIGN(val, align) (((val) + (align) - 1) & ~((align) - 1)) 42 | 43 | struct build_id_note { 44 | ElfW(Nhdr) nhdr; 45 | 46 | char name[4]; 47 | uint8_t build_id[0]; 48 | }; 49 | 50 | enum tag { BY_NAME, BY_SYMBOL }; 51 | 52 | struct callback_data { 53 | union { 54 | const char *name; 55 | 56 | /* Base address of shared object, taken from Dl_info::dli_fbase */ 57 | const void *dli_fbase; 58 | }; 59 | enum tag tag; 60 | struct build_id_note *note; 61 | }; 62 | 63 | static int 64 | build_id_find_nhdr_callback(struct dl_phdr_info *info, size_t size, void *data_) 65 | { 66 | struct callback_data *data = data_; 67 | 68 | if (data->tag == BY_NAME) 69 | if (!info->dlpi_name || strcmp(info->dlpi_name, data->name) != 0) 70 | return 0; 71 | 72 | if (data->tag == BY_SYMBOL) { 73 | /* Calculate address where shared object is mapped into the process space. 74 | * (Using the base address and the virtual address of the first LOAD segment) 75 | */ 76 | void *map_start = NULL; 77 | for (unsigned i = 0; i < info->dlpi_phnum; i++) { 78 | if (info->dlpi_phdr[i].p_type == PT_LOAD) { 79 | map_start = (void *)(info->dlpi_addr + info->dlpi_phdr[i].p_vaddr); 80 | break; 81 | } 82 | } 83 | 84 | if (map_start != data->dli_fbase) 85 | return 0; 86 | } 87 | 88 | for (unsigned i = 0; i < info->dlpi_phnum; i++) { 89 | if (info->dlpi_phdr[i].p_type != PT_NOTE) 90 | continue; 91 | 92 | struct build_id_note *note = (void *)(info->dlpi_addr + 93 | info->dlpi_phdr[i].p_vaddr); 94 | ptrdiff_t len = info->dlpi_phdr[i].p_filesz; 95 | 96 | while (len >= sizeof(struct build_id_note)) { 97 | if (note->nhdr.n_type == NT_GNU_BUILD_ID && 98 | note->nhdr.n_descsz != 0 && 99 | note->nhdr.n_namesz == 4 && 100 | memcmp(note->name, "GNU", 4) == 0) { 101 | data->note = note; 102 | return 1; 103 | } 104 | 105 | size_t offset = sizeof(ElfW(Nhdr)) + 106 | ALIGN(note->nhdr.n_namesz, 4) + 107 | ALIGN(note->nhdr.n_descsz, 4); 108 | note = (struct build_id_note *)((char *)note + offset); 109 | len -= offset; 110 | } 111 | } 112 | 113 | return 0; 114 | } 115 | 116 | const struct build_id_note * 117 | build_id_find_nhdr_by_name(const char *name) 118 | { 119 | struct callback_data data = { 120 | .tag = BY_NAME, 121 | .name = name, 122 | .note = NULL, 123 | }; 124 | 125 | if (!dl_iterate_phdr(build_id_find_nhdr_callback, &data)) 126 | return NULL; 127 | 128 | return data.note; 129 | } 130 | 131 | const struct build_id_note * 132 | build_id_find_nhdr_by_symbol(const void *symbol) 133 | { 134 | Dl_info info; 135 | 136 | if (!dladdr(symbol, &info)) 137 | return NULL; 138 | 139 | if (!info.dli_fbase) 140 | return NULL; 141 | 142 | struct callback_data data = { 143 | .tag = BY_SYMBOL, 144 | .dli_fbase = info.dli_fbase, 145 | .note = NULL, 146 | }; 147 | 148 | if (!dl_iterate_phdr(build_id_find_nhdr_callback, &data)) 149 | return NULL; 150 | 151 | return data.note; 152 | } 153 | 154 | ElfW(Word) 155 | build_id_length(const struct build_id_note *note) 156 | { 157 | return note->nhdr.n_descsz; 158 | } 159 | 160 | const uint8_t * 161 | build_id_data(const struct build_id_note *note) 162 | { 163 | return note->build_id; 164 | } 165 | -------------------------------------------------------------------------------- /build-id.h: -------------------------------------------------------------------------------- 1 | /* vim: set expandtab tabstop=4 softtabstop=4 shiftwidth=4: */ 2 | /* 3 | * Copyright © 2016 Intel Corporation 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the "Software"), 7 | * to deal in the Software without restriction, including without limitation 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | * and/or sell copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice (including the next 13 | * paragraph) shall be included in all copies or substantial portions of the 14 | * Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | * IN THE SOFTWARE. 23 | */ 24 | 25 | #define _GNU_SOURCE 26 | #include 27 | 28 | struct build_id_note; 29 | 30 | const struct build_id_note * 31 | build_id_find_nhdr_by_name(const char *name); 32 | 33 | const struct build_id_note * 34 | build_id_find_nhdr_by_symbol(const void *symbol); 35 | 36 | ElfW(Word) 37 | build_id_length(const struct build_id_note *note); 38 | 39 | const uint8_t * 40 | build_id_data(const struct build_id_note *note); 41 | -------------------------------------------------------------------------------- /build_shared.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | set -x 4 | 5 | prepare_centos() { 6 | if [[ $(rpm --eval '%{centos_ver}') == "7" ]]; then 7 | yum -y install centos-release-scl 8 | yum -y install devtoolset-9 patch wget git make sudo 9 | set +eu 10 | source scl_source enable devtoolset-9 11 | set -eu 12 | elif [[ $(rpm --eval '%{centos_ver}') == "8" ]]; then 13 | dnf install -y gcc-toolset-9-toolchain patch wget git make sudo 14 | dnf install -y yum-utils 15 | set +eu 16 | source /opt/rh/gcc-toolset-9/enable 17 | set -eu 18 | fi 19 | } 20 | 21 | symbol_addr() { 22 | nm $NGINX_BIN | awk '{if($3=="'"$1"'")print "0x" $1}' 23 | } 24 | 25 | generate_offsets() { 26 | addr0=$(symbol_addr ngx_cycle) 27 | printf "#define NGX_THREAD_POOL_DONE_OFFSET 0x%X\n" $(($(symbol_addr ngx_thread_pool_done)-$addr0)) 28 | printf "#define NGX_THREAD_POOL_DONE_LOCK_OFFSET 0x%X\n" $(($(symbol_addr ngx_thread_pool_done_lock)-$addr0)) 29 | addr0=$(symbol_addr ngx_calloc) 30 | printf "#define NGX_THREAD_POOL_HANDLER_OFFSET 0x%X\n" $(($(symbol_addr ngx_thread_pool_handler)-$addr0)) 31 | } 32 | 33 | . /etc/os-release 34 | 35 | if [[ $ID == "centos" ]]; then 36 | prepare_centos 37 | fi 38 | 39 | NGX_BUILD=$(find $OR_SRC/build -maxdepth 1 -name 'nginx-*' -type d) 40 | if [[ -z "$NGX_BUILD" ]]; then 41 | echo 'You need to configure $OR_SRC first.' 42 | exit 1 43 | fi 44 | NGINX_BIN="${NGINX_BIN:-$NGX_BUILD/objs/nginx}" 45 | if [[ ! -x "$NGINX_BIN" ]]; then 46 | echo 'You need to specify $NGINX_BIN.' 47 | exit 1 48 | fi 49 | generate_offsets > symbols.h 50 | 51 | BUILD_ID=$(file $NGINX_BIN | sed -En 's/.*BuildID\[sha1\]=([a-z0-9]+).*/\1/p') 52 | if [[ -z "$BUILD_ID" ]]; then 53 | echo "$NGINX_BIN has no build-id." 54 | exit 1 55 | fi 56 | echo "#define NGX_BUILD_ID \"$BUILD_ID\"" >> symbols.h 57 | 58 | export NGX_LUA_SRC="$(find $OR_SRC/build/ -maxdepth 1 -name 'ngx_lua-*' -type d)/src" 59 | export NGX_OBJS_MAKEFILE="$NGX_BUILD/objs/Makefile" 60 | 61 | export SRC=$PWD 62 | (cd $NGX_BUILD; make -f $SRC/Makefile) 63 | -------------------------------------------------------------------------------- /examples/go/.gitignore: -------------------------------------------------------------------------------- 1 | *.so 2 | *.h 3 | logs 4 | -------------------------------------------------------------------------------- /examples/go/Makefile: -------------------------------------------------------------------------------- 1 | NGINX_BIN ?= /opt/resty_ffi/nginx/sbin/nginx 2 | 3 | .PHONY: build 4 | build: 5 | go mod download 6 | go build -buildmode=c-shared -o libffi_go_echo.so echo.go 7 | 8 | .PHONY: run 9 | run: 10 | @bash -c '[[ -d logs ]] || mkdir logs' 11 | LD_LIBRARY_PATH=$(PWD) $(NGINX_BIN) -p $(PWD) -c nginx.conf 12 | 13 | .PHONY: test 14 | test: 15 | curl localhost:20000/echo 16 | -------------------------------------------------------------------------------- /examples/go/README.md: -------------------------------------------------------------------------------- 1 | # Golang ffi library examples 2 | 3 | * `echo.go` Minimal example, echo your request. 4 | * [lua-resty-ffi-etcd](https://github.com/kingluo/lua-resty-ffi-etcd) 5 | * [lua-resty-ffi-kafka](https://github.com/kingluo/lua-resty-ffi-kafka) 6 | * [lua-resty-ffi-req](https://github.com/kingluo/lua-resty-ffi-req) 7 | 8 | ## Demo 9 | 10 | ```bash 11 | # install golang 12 | # https://go.dev/doc/install 13 | wget https://go.dev/dl/go1.19.3.linux-amd64.tar.gz 14 | rm -rf /usr/local/go && tar -C /usr/local -xzf go1.19.3.linux-amd64.tar.gz 15 | export PATH=$PATH:/usr/local/go/bin 16 | 17 | # compile example libraries 18 | make 19 | 20 | # run nginx 21 | make run 22 | # or specify nginx executable file path 23 | # make run NGINX_BIN=/path/to/nginx 24 | 25 | # in another terminal 26 | # curl the tests 27 | make test 28 | ``` 29 | 30 | ## Library Skelton 31 | 32 | ```go 33 | //export libffi_init 34 | func libffi_init(cfg_cstr *C.char, tq unsafe.Pointer) C.int { 35 | go func() { 36 | for { 37 | // poll a task 38 | task := C.ngx_http_lua_ffi_task_poll(tq) 39 | // if task queue is done, then quit 40 | if task == nil { 41 | break 42 | } 43 | // get the request message from the task 44 | var rlen C.int 45 | r := C.ngx_http_lua_ffi_get_req(task, &rlen) 46 | 47 | // copy the request as response, allocated by C malloc() 48 | // note that both request and response would be freed by nginx 49 | res := C.malloc(C.ulong(rlen)) 50 | C.memcpy(res, unsafe.Pointer(r), C.ulong(rlen)) 51 | C.ngx_http_lua_ffi_respond(task, 0, (*C.char)(res), rlen) 52 | } 53 | }() 54 | return 0 55 | } 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /examples/go/benchmark.lua: -------------------------------------------------------------------------------- 1 | local cjson = require("cjson") 2 | local ffi = require("ffi") 3 | local str_byte = string.byte 4 | local bit = require("bit") 5 | local band = bit.band 6 | local rshift = bit.rshift 7 | local ffi_str = ffi.string 8 | 9 | local _M = {} 10 | 11 | function _M.run(n, arr) 12 | ngx.say("loop times=", n, ", lengths=", cjson.encode(arr)) 13 | ngx.flush(true) 14 | 15 | for _, blen in ipairs(arr) do 16 | local buf = string.rep("A", blen) 17 | local buflen = #buf 18 | assert(buflen==blen) 19 | ngx.say("-- benchmark len=", buflen) 20 | 21 | local demo = ngx.load_ffi("ffi_go_echo") 22 | ngx.update_time() 23 | local t1 = ngx.now() 24 | for _ = 1,n do 25 | local ok, res = demo:echo(buf) 26 | assert(ok) 27 | assert(res==buf) 28 | end 29 | ngx.update_time() 30 | local t2 = ngx.now() 31 | ngx.say("ffi: ", t2-t1) 32 | ngx.flush(true) 33 | 34 | -- unix domain socket 35 | local sock = ngx.socket.tcp() 36 | local ok, err = sock:connect("unix:/tmp/echo.sock") 37 | assert(ok) 38 | 39 | local tbuf = {} 40 | local hdr_buf = ffi.new("unsigned char[4]") 41 | 42 | ngx.update_time() 43 | local t1 = ngx.now() 44 | for _ = 1,n do 45 | local len = buflen 46 | for i = 3, 0, -1 do 47 | hdr_buf[i] = band(len, 255) 48 | len = rshift(len, 8) 49 | end 50 | 51 | tbuf[1] = ffi_str(hdr_buf, 4) 52 | tbuf[2] = buf 53 | local sent = sock:send(tbuf) 54 | assert(sent==buflen+4) 55 | 56 | local hdr = sock:receive(4) 57 | local hi, mi, li, lli = str_byte(hdr, 1, 4) 58 | local len = 256 * (256 * (256 * hi + mi) + li) + lli 59 | assert(len==buflen) 60 | local data = sock:receive(len) 61 | assert(data==buf) 62 | end 63 | ngx.update_time() 64 | local t2 = ngx.now() 65 | ngx.say("uds: ", t2-t1) 66 | sock:close() 67 | ngx.flush(true) 68 | end 69 | end 70 | 71 | return _M 72 | -------------------------------------------------------------------------------- /examples/go/echo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | #cgo LDFLAGS: -shared 5 | #include 6 | void* ngx_http_lua_ffi_task_poll(void *p); 7 | char* ngx_http_lua_ffi_get_req(void *tsk, int *len); 8 | void ngx_http_lua_ffi_respond(void *tsk, int rc, char* rsp, int rsp_len); 9 | */ 10 | import "C" 11 | import ( 12 | "log" 13 | "unsafe" 14 | ) 15 | 16 | //export libffi_init 17 | func libffi_init(cfg_cstr *C.char, tq unsafe.Pointer) C.int { 18 | log.Println("start go echo runtime") 19 | go func() { 20 | for { 21 | task := C.ngx_http_lua_ffi_task_poll(tq) 22 | if task == nil { 23 | break 24 | } 25 | var rlen C.int 26 | r := C.ngx_http_lua_ffi_get_req(task, &rlen) 27 | res := C.malloc(C.ulong(rlen)) 28 | C.memcpy(res, unsafe.Pointer(r), C.ulong(rlen)) 29 | C.ngx_http_lua_ffi_respond(task, 0, (*C.char)(res), rlen) 30 | } 31 | log.Println("exit go echo runtime") 32 | }() 33 | return 0 34 | } 35 | 36 | func main() {} 37 | -------------------------------------------------------------------------------- /examples/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kingluo/lua-resty-ffi 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /examples/go/go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingluo/lua-resty-ffi/33b66308a006a8d2461a7186949255a7c5375428/examples/go/go.sum -------------------------------------------------------------------------------- /examples/go/nginx.conf: -------------------------------------------------------------------------------- 1 | daemon off; 2 | error_log /dev/stderr info; 3 | worker_processes auto; 4 | env LD_LIBRARY_PATH; 5 | 6 | events {} 7 | 8 | http { 9 | server { 10 | listen 20000; 11 | 12 | location /echo { 13 | content_by_lua_block { 14 | require("resty_ffi") 15 | local demo = ngx.load_ffi("ffi_go_echo") 16 | local ok, res = demo:echo("foobar") 17 | assert(ok) 18 | assert(res == "foobar") 19 | 20 | demo:__unload() 21 | ok, res = demo:echo("foobar") 22 | assert(not ok) 23 | ngx.log(ngx.ERR, res) 24 | 25 | ngx.say("ok") 26 | } 27 | } 28 | 29 | location /benchmark { 30 | content_by_lua_block { 31 | require("resty_ffi") 32 | local n = ngx.var.arg_times or 100000 33 | local arr = ngx.var.arg_arr 34 | local t = {16, 128, 1024, 1024*10, 1024*100} 35 | if arr then 36 | t = {} 37 | for str in string.gmatch(arr, "([^,]+)") do 38 | table.insert(t, tonumber(str)) 39 | end 40 | end 41 | require("benchmark").run(n, t) 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/go/unix_socket_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "log" 6 | "net" 7 | "os" 8 | "syscall" 9 | ) 10 | 11 | const SockAddr = "/tmp/echo.sock" 12 | 13 | func echoServer(c net.Conn) { 14 | header := make([]byte, 4) 15 | buf := make([]byte, 1024*1024*10) 16 | out := make([]byte, 1024*1024*10) 17 | for { 18 | n, err := c.Read(header) 19 | if err != nil { 20 | break 21 | } 22 | length := binary.BigEndian.Uint32(header) 23 | read := 0 24 | for uint32(read) < length { 25 | n, err = c.Read(buf[read:length]) 26 | if err != nil { 27 | break 28 | } 29 | read += n 30 | } 31 | 32 | binary.BigEndian.PutUint32(out[:4], length) 33 | copy(out[4:], buf[:length]) 34 | sent := 0 35 | for uint32(sent) < length+4 { 36 | n, err = c.Write(out[sent : length+4]) 37 | if err != nil { 38 | break 39 | } 40 | sent += n 41 | } 42 | } 43 | c.Close() 44 | } 45 | 46 | func main() { 47 | if err := os.RemoveAll(SockAddr); err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | syscall.Umask(0) 52 | l, err := net.Listen("unix", SockAddr) 53 | if err != nil { 54 | log.Fatal("listen error:", err) 55 | } 56 | defer l.Close() 57 | for { 58 | conn, err := l.Accept() 59 | if err != nil { 60 | log.Fatal("accept error:", err) 61 | } 62 | 63 | go echoServer(conn) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/java/.gitignore: -------------------------------------------------------------------------------- 1 | *_temp/ 2 | *.so 3 | *.h 4 | logs 5 | -------------------------------------------------------------------------------- /examples/java/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | @bash build.sh 4 | 5 | .PHONY: run 6 | run: 7 | @bash run.sh 8 | 9 | .PHONY: test 10 | test: 11 | curl localhost:20000/echo 12 | curl localhost:20000/http2 13 | -------------------------------------------------------------------------------- /examples/java/README.md: -------------------------------------------------------------------------------- 1 | # JAVA ffi library 2 | 3 | It uses [`jnr-ffi`](https://github.com/jnr/jnr-ffi) to call lua-resty-ffi C APIs. 4 | 5 | * `echo-demo` 6 | 7 | Echo the request. 8 | 9 | * `http2-demo` 10 | 11 | Simple http2 client. 12 | 13 | ## Install openjdk-11 14 | 15 | ```bash 16 | # for centos 17 | yum -y install java-11-openjdk-devel 18 | 19 | # for ubuntu 20 | apt -y install openjdk-11-jdk 21 | ``` 22 | 23 | ## Build and test 24 | 25 | ```bash 26 | # compile loader library 27 | make 28 | 29 | # run nginx 30 | make run 31 | # or specify nginx executable file path 32 | # make run NGINX_BIN=/path/to/nginx 33 | 34 | # in another terminal 35 | # curl the tests 36 | make test 37 | ``` 38 | 39 | ## Library Skelton 40 | 41 | ```java 42 | class PollThread implements Runnable { 43 | ... 44 | public void run() { 45 | Libc libc = LibraryLoader.create(Libc.class).load("c"); 46 | FFI ffi = LibraryLoader.create(FFI.class).load(LibraryLoader.DEFAULT_LIBRARY); 47 | var tq = Pointer.wrap(jnr.ffi.Runtime.getSystemRuntime(), this.tq); 48 | IntByReference ap = new IntByReference(); 49 | 50 | while(true) { 51 | // poll a task 52 | var task = ffi.ngx_http_lua_ffi_task_poll(tq); 53 | // if the task queue is done, then quit 54 | if (task == null) { 55 | break; 56 | } 57 | 58 | // get the request from the task 59 | var req = ffi.ngx_http_lua_ffi_get_req(task, ap); 60 | var len = ap.intValue(); 61 | 62 | // copy the request as response, allocated by C malloc() 63 | // note that both request and response would be freed by nginx 64 | var rsp = libc.malloc(len); 65 | libc.memcpy(rsp, req, len); 66 | ffi.ngx_http_lua_ffi_respond(task, 0, rsp, len); 67 | } 68 | 69 | System.out.println("exit java echo runtime"); 70 | } 71 | } 72 | 73 | public class App 74 | { 75 | // implement an entry function 76 | public static int init(String cfg, long tq) { 77 | // launch a thread to poll tasks 78 | var pollThread = new PollThread(tq); 79 | Thread thread = new Thread(pollThread); 80 | thread.setDaemon(true); 81 | thread.start(); 82 | return 0; 83 | } 84 | } 85 | ``` 86 | 87 | Specify the entry class and function in lua and use it: 88 | 89 | ```lua 90 | local demo = ngx.load_ffi("ffi_java", "demo.echo.App,init,") 91 | local ok, res = demo:echo("hello") 92 | assert(ok) 93 | assert(res == "hello") 94 | 95 | -- You could use '?' as class suffix to indicate that 96 | -- all classes within the package of the target class are hot-reload. 97 | -- When the class gets loaded next time (after previous unload), 98 | -- it would reload the class instead of the cached version. 99 | local demo = ngx.load_ffi("ffi_java", "demo.echo.App?,init,") 100 | ``` 101 | -------------------------------------------------------------------------------- /examples/java/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | set -x 4 | 5 | . /etc/os-release 6 | 7 | if [[ $ID == "centos" ]]; then 8 | set +eu 9 | if [[ $VERSION_ID == "7" ]]; then 10 | source scl_source enable devtoolset-9 11 | elif [[ $VERSION_ID == "8" ]]; then 12 | source /opt/rh/gcc-toolset-9/enable 13 | fi 14 | set -eu 15 | gcc -O2 -shared -fPIC -o libffi_java.so loader.c -I/usr/lib/jvm/java-11-openjdk/include/ -I/usr/lib/jvm/java-11-openjdk/include/linux/ -L/usr/lib/jvm/java-11-openjdk/lib/ -L/usr/lib/jvm/java-11-openjdk/lib/server/ -ljvm -pthread 16 | else 17 | gcc -O2 -shared -fPIC -o libffi_java.so loader.c -I/usr/lib/jvm/java-11-openjdk-amd64/include/ -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux/ -L/usr/lib/jvm/java-11-openjdk-amd64/lib/ -L/usr/lib/jvm/java-11-openjdk-amd64/lib/server/ -ljvm -pthread 18 | fi 19 | 20 | (cd http2-demo; ./mvnw -q -DskipTests=true -Dmaven.test.skip=true package) 21 | (cd echo-demo; ./mvnw -q -DskipTests=true -Dmaven.test.skip=true package) 22 | (cd loader; ./mvnw -q -DskipTests=true -Dmaven.test.skip=true package) 23 | -------------------------------------------------------------------------------- /examples/java/echo-demo/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /examples/java/echo-demo/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingluo/lua-resty-ffi/33b66308a006a8d2461a7186949255a7c5375428/examples/java/echo-demo/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /examples/java/echo-demo/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /examples/java/echo-demo/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.1.1 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "`uname`" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=`java-config --jre-home` 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && 89 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="`which javac`" 94 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=`which readlink` 97 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 98 | if $darwin ; then 99 | javaHome="`dirname \"$javaExecutable\"`" 100 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 101 | else 102 | javaExecutable="`readlink -f \"$javaExecutable\"`" 103 | fi 104 | javaHome="`dirname \"$javaExecutable\"`" 105 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="`\\unset -f command; \\command -v java`" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=`cd "$wdir/.."; pwd` 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir"; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | echo "$(tr -s '\n' ' ' < "$1")" 164 | fi 165 | } 166 | 167 | BASE_DIR=$(find_maven_basedir "$(dirname $0)") 168 | if [ -z "$BASE_DIR" ]; then 169 | exit 1; 170 | fi 171 | 172 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | echo $MAVEN_PROJECTBASEDIR 175 | fi 176 | 177 | ########################################################################################## 178 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 179 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 180 | ########################################################################################## 181 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 182 | if [ "$MVNW_VERBOSE" = true ]; then 183 | echo "Found .mvn/wrapper/maven-wrapper.jar" 184 | fi 185 | else 186 | if [ "$MVNW_VERBOSE" = true ]; then 187 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 188 | fi 189 | if [ -n "$MVNW_REPOURL" ]; then 190 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 191 | else 192 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 193 | fi 194 | while IFS="=" read key value; do 195 | case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;; 196 | esac 197 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 198 | if [ "$MVNW_VERBOSE" = true ]; then 199 | echo "Downloading from: $wrapperUrl" 200 | fi 201 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 202 | if $cygwin; then 203 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 204 | fi 205 | 206 | if command -v wget > /dev/null; then 207 | QUIET="--quiet" 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found wget ... using wget" 210 | QUIET="" 211 | fi 212 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 213 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" 214 | else 215 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" 216 | fi 217 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 218 | elif command -v curl > /dev/null; then 219 | QUIET="--silent" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Found curl ... using curl" 222 | QUIET="" 223 | fi 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L 228 | fi 229 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 230 | else 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Falling back to using Java to download" 233 | fi 234 | javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 235 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" 236 | # For Cygwin, switch paths to Windows format before running javac 237 | if $cygwin; then 238 | javaSource=`cygpath --path --windows "$javaSource"` 239 | javaClass=`cygpath --path --windows "$javaClass"` 240 | fi 241 | if [ -e "$javaSource" ]; then 242 | if [ ! -e "$javaClass" ]; then 243 | if [ "$MVNW_VERBOSE" = true ]; then 244 | echo " - Compiling MavenWrapperDownloader.java ..." 245 | fi 246 | # Compiling the Java class 247 | ("$JAVA_HOME/bin/javac" "$javaSource") 248 | fi 249 | if [ -e "$javaClass" ]; then 250 | # Running the downloader 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo " - Running MavenWrapperDownloader.java ..." 253 | fi 254 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 255 | fi 256 | fi 257 | fi 258 | fi 259 | ########################################################################################## 260 | # End of extension 261 | ########################################################################################## 262 | 263 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 264 | 265 | # For Cygwin, switch paths to Windows format before running java 266 | if $cygwin; then 267 | [ -n "$JAVA_HOME" ] && 268 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 269 | [ -n "$CLASSPATH" ] && 270 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 271 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 272 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 273 | fi 274 | 275 | # Provide a "standardized" way to retrieve the CLI args that will 276 | # work with both Windows and non-Windows executions. 277 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 278 | export MAVEN_CMD_LINE_ARGS 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | $MAVEN_DEBUG_OPTS \ 285 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 286 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 287 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 288 | -------------------------------------------------------------------------------- /examples/java/echo-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | demo.App 8 | echo-demo 9 | 1.0-SNAPSHOT 10 | 11 | echo-demo 12 | 13 | http://www.example.com 14 | 15 | 16 | UTF-8 17 | 11 18 | 11 19 | 20 | 21 | 22 | 23 | com.github.jnr 24 | jnr-ffi 25 | 2.2.13 26 | 27 | 28 | 29 | 30 | 31 | 32 | maven-assembly-plugin 33 | 34 | 35 | package 36 | 37 | single 38 | 39 | 40 | 41 | 42 | 43 | jar-with-dependencies 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/java/echo-demo/src/main/java/demo/echo/App.java: -------------------------------------------------------------------------------- 1 | package demo.echo; 2 | 3 | import jnr.ffi.LibraryLoader; 4 | import jnr.ffi.*; 5 | import jnr.ffi.byref.*; 6 | import jnr.ffi.annotations.In; 7 | import jnr.ffi.annotations.Out; 8 | 9 | class PollThread implements Runnable { 10 | public interface FFI { 11 | Pointer ngx_http_lua_ffi_task_poll(@In Pointer tq); 12 | Pointer ngx_http_lua_ffi_get_req(@In Pointer task, @Out IntByReference len); 13 | void ngx_http_lua_ffi_respond(@In Pointer tsk, @In int rc, @In Pointer rsp, @In int rsp_len); 14 | } 15 | 16 | public interface Libc { 17 | Pointer malloc(int size); 18 | Pointer memcpy(Pointer dest, Pointer src, int n); 19 | } 20 | 21 | public long tq; 22 | 23 | public PollThread(long tq) { 24 | this.tq = tq; 25 | } 26 | 27 | public void run() { 28 | Libc libc = LibraryLoader.create(Libc.class).load("c"); 29 | FFI ffi = LibraryLoader.create(FFI.class).load(LibraryLoader.DEFAULT_LIBRARY); 30 | var tq = Pointer.wrap(jnr.ffi.Runtime.getSystemRuntime(), this.tq); 31 | IntByReference ap = new IntByReference(); 32 | 33 | while(true) { 34 | var task = ffi.ngx_http_lua_ffi_task_poll(tq); 35 | if (task == null) { 36 | break; 37 | } 38 | var req = ffi.ngx_http_lua_ffi_get_req(task, ap); 39 | var len = ap.intValue(); 40 | 41 | var rsp = libc.malloc(len); 42 | libc.memcpy(rsp, req, len); 43 | ffi.ngx_http_lua_ffi_respond(task, 0, rsp, len); 44 | } 45 | 46 | System.out.println("exit java echo runtime"); 47 | } 48 | } 49 | 50 | public class App 51 | { 52 | public static int init(String cfg, long tq) { 53 | var pollThread = new PollThread(tq); 54 | Thread thread = new Thread(pollThread); 55 | thread.setDaemon(true); 56 | thread.start(); 57 | return 0; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/java/http2-demo/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /examples/java/http2-demo/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingluo/lua-resty-ffi/33b66308a006a8d2461a7186949255a7c5375428/examples/java/http2-demo/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /examples/java/http2-demo/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /examples/java/http2-demo/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.1.1 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "`uname`" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=`java-config --jre-home` 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && 89 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="`which javac`" 94 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=`which readlink` 97 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 98 | if $darwin ; then 99 | javaHome="`dirname \"$javaExecutable\"`" 100 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 101 | else 102 | javaExecutable="`readlink -f \"$javaExecutable\"`" 103 | fi 104 | javaHome="`dirname \"$javaExecutable\"`" 105 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="`\\unset -f command; \\command -v java`" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=`cd "$wdir/.."; pwd` 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir"; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | echo "$(tr -s '\n' ' ' < "$1")" 164 | fi 165 | } 166 | 167 | BASE_DIR=$(find_maven_basedir "$(dirname $0)") 168 | if [ -z "$BASE_DIR" ]; then 169 | exit 1; 170 | fi 171 | 172 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | echo $MAVEN_PROJECTBASEDIR 175 | fi 176 | 177 | ########################################################################################## 178 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 179 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 180 | ########################################################################################## 181 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 182 | if [ "$MVNW_VERBOSE" = true ]; then 183 | echo "Found .mvn/wrapper/maven-wrapper.jar" 184 | fi 185 | else 186 | if [ "$MVNW_VERBOSE" = true ]; then 187 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 188 | fi 189 | if [ -n "$MVNW_REPOURL" ]; then 190 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 191 | else 192 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 193 | fi 194 | while IFS="=" read key value; do 195 | case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;; 196 | esac 197 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 198 | if [ "$MVNW_VERBOSE" = true ]; then 199 | echo "Downloading from: $wrapperUrl" 200 | fi 201 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 202 | if $cygwin; then 203 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 204 | fi 205 | 206 | if command -v wget > /dev/null; then 207 | QUIET="--quiet" 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found wget ... using wget" 210 | QUIET="" 211 | fi 212 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 213 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" 214 | else 215 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" 216 | fi 217 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 218 | elif command -v curl > /dev/null; then 219 | QUIET="--silent" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Found curl ... using curl" 222 | QUIET="" 223 | fi 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L 228 | fi 229 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 230 | else 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Falling back to using Java to download" 233 | fi 234 | javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 235 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" 236 | # For Cygwin, switch paths to Windows format before running javac 237 | if $cygwin; then 238 | javaSource=`cygpath --path --windows "$javaSource"` 239 | javaClass=`cygpath --path --windows "$javaClass"` 240 | fi 241 | if [ -e "$javaSource" ]; then 242 | if [ ! -e "$javaClass" ]; then 243 | if [ "$MVNW_VERBOSE" = true ]; then 244 | echo " - Compiling MavenWrapperDownloader.java ..." 245 | fi 246 | # Compiling the Java class 247 | ("$JAVA_HOME/bin/javac" "$javaSource") 248 | fi 249 | if [ -e "$javaClass" ]; then 250 | # Running the downloader 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo " - Running MavenWrapperDownloader.java ..." 253 | fi 254 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 255 | fi 256 | fi 257 | fi 258 | fi 259 | ########################################################################################## 260 | # End of extension 261 | ########################################################################################## 262 | 263 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 264 | 265 | # For Cygwin, switch paths to Windows format before running java 266 | if $cygwin; then 267 | [ -n "$JAVA_HOME" ] && 268 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 269 | [ -n "$CLASSPATH" ] && 270 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 271 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 272 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 273 | fi 274 | 275 | # Provide a "standardized" way to retrieve the CLI args that will 276 | # work with both Windows and non-Windows executions. 277 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 278 | export MAVEN_CMD_LINE_ARGS 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | $MAVEN_DEBUG_OPTS \ 285 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 286 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 287 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 288 | -------------------------------------------------------------------------------- /examples/java/http2-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | demo.App 8 | http2-demo 9 | 1.0-SNAPSHOT 10 | 11 | http2-demo 12 | 13 | http://www.example.com 14 | 15 | 16 | UTF-8 17 | 11 18 | 11 19 | 20 | 21 | 22 | 23 | com.google.code.gson 24 | gson 25 | 2.9.1 26 | 27 | 28 | com.github.jnr 29 | jnr-ffi 30 | 2.2.13 31 | 32 | 33 | 34 | 35 | 36 | 37 | maven-assembly-plugin 38 | 39 | 40 | package 41 | 42 | single 43 | 44 | 45 | 46 | 47 | 48 | jar-with-dependencies 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/java/http2-demo/src/main/java/demo/http2/App.java: -------------------------------------------------------------------------------- 1 | package demo.http2; 2 | 3 | import jnr.ffi.LibraryLoader; 4 | import jnr.ffi.*; 5 | import jnr.ffi.byref.*; 6 | import jnr.ffi.annotations.In; 7 | import jnr.ffi.annotations.Out; 8 | 9 | import java.net.URI; 10 | import java.net.http.HttpClient; 11 | import java.net.http.HttpHeaders; 12 | import java.net.http.HttpRequest; 13 | import java.net.http.HttpRequest.BodyPublishers; 14 | import java.net.http.HttpResponse; 15 | import java.net.http.HttpClient.Version; 16 | import java.nio.charset.StandardCharsets; 17 | import java.util.LinkedHashMap; 18 | 19 | import com.google.gson.Gson; 20 | 21 | class Request { 22 | public String method; 23 | public String uri; 24 | public LinkedHashMap headers; 25 | public String body; 26 | } 27 | 28 | class Response { 29 | public LinkedHashMap headers = new LinkedHashMap<>(); 30 | public String body; 31 | } 32 | 33 | class PollThread implements Runnable { 34 | public interface FFI { 35 | Pointer ngx_http_lua_ffi_task_poll(@In Pointer tq); 36 | Pointer ngx_http_lua_ffi_get_req(@In Pointer task, @Out IntByReference len); 37 | void ngx_http_lua_ffi_respond(@In Pointer tsk, @In int rc, @In Pointer rsp, @In int rsp_len); 38 | } 39 | 40 | public interface Libc { 41 | Pointer malloc(int size); 42 | } 43 | 44 | public long tq; 45 | 46 | public PollThread(long tq) { 47 | this.tq = tq; 48 | } 49 | 50 | public void run() { 51 | Libc libc = LibraryLoader.create(Libc.class).load("c"); 52 | FFI ffi = LibraryLoader.create(FFI.class).load(LibraryLoader.DEFAULT_LIBRARY); 53 | var tq = Pointer.wrap(jnr.ffi.Runtime.getSystemRuntime(), this.tq); 54 | IntByReference ap = new IntByReference(); 55 | 56 | var httpClient = HttpClient.newBuilder().version(Version.HTTP_2).build(); 57 | var gson = new Gson(); 58 | 59 | while(true) { 60 | var task = ffi.ngx_http_lua_ffi_task_poll(tq); 61 | if (task == null) { 62 | break; 63 | } 64 | var req = ffi.ngx_http_lua_ffi_get_req(task, ap); 65 | var len = ap.intValue(); 66 | 67 | final byte[] bytes = new byte[len]; 68 | req.get(0, bytes, 0, len); 69 | String str = new String(bytes, StandardCharsets.UTF_8); 70 | 71 | var data = gson.fromJson(str, Request.class); 72 | var builder = HttpRequest.newBuilder().uri(URI.create(data.uri)); 73 | builder.method(data.method, BodyPublishers.ofString(data.body)); 74 | if (data.headers != null) { 75 | for (var entry : data.headers.entrySet()) { 76 | builder.header(entry.getKey(), entry.getValue()); 77 | } 78 | } 79 | 80 | httpClient.sendAsync(builder.build(), HttpResponse.BodyHandlers.ofString()) 81 | .thenAccept(res -> { 82 | Response rsp = new Response(); 83 | HttpHeaders headers = res.headers(); 84 | headers.map().forEach((key, values) -> { 85 | rsp.headers.put(key, String.join(",", values)); 86 | }); 87 | rsp.body = res.body(); 88 | String json = gson.toJson(rsp); 89 | 90 | byte[] arr = json.getBytes(); 91 | int rspLen = arr.length; 92 | var rspStr = libc.malloc(rspLen); 93 | rspStr.put(0, arr, 0, rspLen); 94 | ffi.ngx_http_lua_ffi_respond(task, 0, rspStr, rspLen); 95 | }); 96 | } 97 | } 98 | } 99 | 100 | public class App 101 | { 102 | public static int init(String cfg, long tq) 103 | { 104 | var pollThread = new PollThread(tq); 105 | Thread thread = new Thread(pollThread); 106 | thread.setDaemon(true); 107 | thread.start(); 108 | return 0; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /examples/java/loader.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Jinhua Luo (kingluo) luajit.io@gmail.com 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its 16 | * contributors may be used to endorse or promote products derived from 17 | * this software without specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | #define _GNU_SOURCE 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | static JavaVM *vm; 39 | static JNIEnv *env; 40 | static pthread_mutex_t init_lock = PTHREAD_MUTEX_INITIALIZER; 41 | 42 | #define RET(rc) \ 43 | pthread_mutex_unlock(&init_lock); \ 44 | return rc; 45 | 46 | int libffi_init(char* cfg, void *tq) 47 | { 48 | pthread_mutex_lock(&init_lock); 49 | 50 | if (vm == NULL) { 51 | JavaVMInitArgs vm_args; 52 | jint res; 53 | jclass cls; 54 | jmethodID mid; 55 | jstring jstr; 56 | jobjectArray main_args; 57 | 58 | #define MAX_OPTS 50 59 | JavaVMOption options[MAX_OPTS]; 60 | vm_args.version = JNI_VERSION_1_8; 61 | vm_args.nOptions = 0; 62 | vm_args.options = options; 63 | 64 | char* path = NULL; 65 | char* class_path = getenv("CLASSPATH"); 66 | if (class_path) { 67 | if (asprintf(&path, "-Djava.class.path=%s", class_path) == -1) { 68 | RET(1); 69 | } 70 | options[vm_args.nOptions++].optionString = path; 71 | } 72 | 73 | char* java_opts = getenv("JAVA_OPTS"); 74 | char* opts = NULL; 75 | if (java_opts) { 76 | opts = strdup(java_opts); 77 | char* opt; 78 | while ((opt = strsep(&opts, " "))) { 79 | printf("opt: %s\n", opt); 80 | options[vm_args.nOptions++].optionString = opt; 81 | if (vm_args.nOptions == MAX_OPTS) { 82 | break; 83 | } 84 | } 85 | } 86 | 87 | res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args); 88 | if (path) { 89 | free(path); 90 | } 91 | if (opts) { 92 | free(opts); 93 | } 94 | if (res != JNI_OK) { 95 | printf("Failed to create Java VM\n"); 96 | RET(1); 97 | } 98 | } 99 | 100 | char* orig_cfg = strdup(cfg); 101 | char* str = cfg; 102 | char* class = strsep(&str, ","); 103 | int dyn_flag = class[strlen(class)-1] == '?'; 104 | if (dyn_flag) { 105 | class = "resty/ffi/Loader"; 106 | } else { 107 | for (char* tmp = class; *tmp != 0; tmp++) { 108 | if (*tmp == '.') { 109 | *tmp = '/'; 110 | } 111 | } 112 | } 113 | 114 | char* method = strsep(&str, ","); 115 | char* cfg_str= str; 116 | 117 | jclass cls; 118 | jmethodID mid; 119 | jstring jcfg; 120 | 121 | cls = (*env)->FindClass(env, class); 122 | if (cls == NULL) { 123 | printf("Failed to find Main class\n"); 124 | free(orig_cfg); 125 | RET(1); 126 | } 127 | 128 | mid = (*env)->GetStaticMethodID(env, cls, method, "(Ljava/lang/String;J)I"); 129 | if (mid == NULL) { 130 | printf("Failed to find main function\n"); 131 | free(orig_cfg); 132 | RET(1); 133 | } 134 | 135 | if (dyn_flag) { 136 | jcfg = (*env)->NewStringUTF(env, orig_cfg); 137 | free(orig_cfg); 138 | } else { 139 | jcfg = (*env)->NewStringUTF(env, cfg_str); 140 | } 141 | int rc = (*env)->CallStaticIntMethod(env, cls, mid, jcfg, (jlong)tq); 142 | (*env)->DeleteLocalRef(env, jcfg); 143 | 144 | /* Check for errors. */ 145 | if ((*env)->ExceptionOccurred(env)) { 146 | (*env)->ExceptionDescribe(env); 147 | } 148 | 149 | pthread_mutex_unlock(&init_lock); 150 | RET(rc); 151 | } 152 | -------------------------------------------------------------------------------- /examples/java/loader/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /examples/java/loader/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingluo/lua-resty-ffi/33b66308a006a8d2461a7186949255a7c5375428/examples/java/loader/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /examples/java/loader/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /examples/java/loader/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.1.1 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "`uname`" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=`java-config --jre-home` 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && 89 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="`which javac`" 94 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=`which readlink` 97 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 98 | if $darwin ; then 99 | javaHome="`dirname \"$javaExecutable\"`" 100 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 101 | else 102 | javaExecutable="`readlink -f \"$javaExecutable\"`" 103 | fi 104 | javaHome="`dirname \"$javaExecutable\"`" 105 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="`\\unset -f command; \\command -v java`" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=`cd "$wdir/.."; pwd` 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir"; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | echo "$(tr -s '\n' ' ' < "$1")" 164 | fi 165 | } 166 | 167 | BASE_DIR=$(find_maven_basedir "$(dirname $0)") 168 | if [ -z "$BASE_DIR" ]; then 169 | exit 1; 170 | fi 171 | 172 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | echo $MAVEN_PROJECTBASEDIR 175 | fi 176 | 177 | ########################################################################################## 178 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 179 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 180 | ########################################################################################## 181 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 182 | if [ "$MVNW_VERBOSE" = true ]; then 183 | echo "Found .mvn/wrapper/maven-wrapper.jar" 184 | fi 185 | else 186 | if [ "$MVNW_VERBOSE" = true ]; then 187 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 188 | fi 189 | if [ -n "$MVNW_REPOURL" ]; then 190 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 191 | else 192 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 193 | fi 194 | while IFS="=" read key value; do 195 | case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;; 196 | esac 197 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 198 | if [ "$MVNW_VERBOSE" = true ]; then 199 | echo "Downloading from: $wrapperUrl" 200 | fi 201 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 202 | if $cygwin; then 203 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 204 | fi 205 | 206 | if command -v wget > /dev/null; then 207 | QUIET="--quiet" 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found wget ... using wget" 210 | QUIET="" 211 | fi 212 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 213 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" 214 | else 215 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" 216 | fi 217 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 218 | elif command -v curl > /dev/null; then 219 | QUIET="--silent" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Found curl ... using curl" 222 | QUIET="" 223 | fi 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L 228 | fi 229 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 230 | else 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Falling back to using Java to download" 233 | fi 234 | javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 235 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" 236 | # For Cygwin, switch paths to Windows format before running javac 237 | if $cygwin; then 238 | javaSource=`cygpath --path --windows "$javaSource"` 239 | javaClass=`cygpath --path --windows "$javaClass"` 240 | fi 241 | if [ -e "$javaSource" ]; then 242 | if [ ! -e "$javaClass" ]; then 243 | if [ "$MVNW_VERBOSE" = true ]; then 244 | echo " - Compiling MavenWrapperDownloader.java ..." 245 | fi 246 | # Compiling the Java class 247 | ("$JAVA_HOME/bin/javac" "$javaSource") 248 | fi 249 | if [ -e "$javaClass" ]; then 250 | # Running the downloader 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo " - Running MavenWrapperDownloader.java ..." 253 | fi 254 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 255 | fi 256 | fi 257 | fi 258 | fi 259 | ########################################################################################## 260 | # End of extension 261 | ########################################################################################## 262 | 263 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 264 | 265 | # For Cygwin, switch paths to Windows format before running java 266 | if $cygwin; then 267 | [ -n "$JAVA_HOME" ] && 268 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 269 | [ -n "$CLASSPATH" ] && 270 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 271 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 272 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 273 | fi 274 | 275 | # Provide a "standardized" way to retrieve the CLI args that will 276 | # work with both Windows and non-Windows executions. 277 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 278 | export MAVEN_CMD_LINE_ARGS 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | $MAVEN_DEBUG_OPTS \ 285 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 286 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 287 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 288 | -------------------------------------------------------------------------------- /examples/java/loader/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | resty.ffi 8 | resty-ffi-loader 9 | 1.0-SNAPSHOT 10 | 11 | resty-ffi-loader 12 | 13 | http://www.example.com 14 | 15 | 16 | UTF-8 17 | 11 18 | 11 19 | 20 | 21 | 22 | 23 | 24 | maven-assembly-plugin 25 | 26 | 27 | package 28 | 29 | single 30 | 31 | 32 | 33 | 34 | 35 | jar-with-dependencies 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/java/loader/src/main/java/resty/ffi/Loader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010-2016 Quan Le. http://quanla.github.io/ 3 | * Copyright (c) 2022, Jinhua Luo (kingluo) luajit.io@gmail.com 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, this 10 | * list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from 18 | * this software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | package resty.ffi; 33 | 34 | import java.io.*; 35 | import java.io.ByteArrayInputStream; 36 | import java.io.File; 37 | import java.io.IOException; 38 | import java.io.InputStream; 39 | import java.sql.Connection; 40 | import java.sql.PreparedStatement; 41 | import java.sql.ResultSet; 42 | import java.sql.SQLException; 43 | import java.util.Arrays; 44 | import java.util.Collection; 45 | import java.util.HashSet; 46 | import java.util.LinkedList; 47 | import java.util.Set; 48 | import java.util.jar.JarFile; 49 | import java.util.zip.ZipEntry; 50 | 51 | /** 52 | * Represent a function that accept one parameter and return value 53 | * @param The only parameter 54 | * @param The return value 55 | */ 56 | interface F1 { 57 | /** 58 | * Evaluate or execute the function 59 | * @param obj The parameter 60 | * @return Result of execution 61 | */ 62 | T e(A obj); 63 | } 64 | 65 | class IOUtil { 66 | /** 67 | * Read the stream into byte array 68 | * @param inputStream 69 | * @return 70 | * @throws java.io.IOException 71 | */ 72 | public static byte[] readData(InputStream inputStream) { 73 | try { 74 | return readDataNice(inputStream); 75 | } finally { 76 | close(inputStream); 77 | } 78 | } 79 | 80 | public static byte[] readDataNice(InputStream inputStream) { 81 | ByteArrayOutputStream boTemp = null; 82 | byte[] buffer = null; 83 | try { 84 | int read; 85 | buffer = new byte[8192]; 86 | boTemp = new ByteArrayOutputStream(); 87 | while ((read=inputStream.read(buffer, 0, 8192)) > -1) { 88 | boTemp.write(buffer, 0, read); 89 | } 90 | return boTemp.toByteArray(); 91 | } catch (IOException e) { 92 | throw new RuntimeException(e); 93 | } 94 | } 95 | 96 | /** 97 | * Close streams (in or out) 98 | * @param stream 99 | */ 100 | public static void close(Closeable stream) { 101 | if (stream != null) { 102 | try { 103 | if (stream instanceof Flushable) { 104 | ((Flushable)stream).flush(); 105 | } 106 | stream.close(); 107 | } catch (IOException e) { 108 | // When the stream is closed or interupted, can ignore this exception 109 | } 110 | } 111 | } 112 | } 113 | 114 | abstract class AggressiveClassLoader extends ClassLoader { 115 | Set loadedClasses = new HashSet<>(); 116 | Set unavaiClasses = new HashSet<>(); 117 | private ClassLoader parent = AggressiveClassLoader.class.getClassLoader(); 118 | String prefix; 119 | 120 | public AggressiveClassLoader(String clsName) { 121 | prefix = clsName; 122 | } 123 | 124 | @Override 125 | public Class loadClass(String name) throws ClassNotFoundException { 126 | if (!name.startsWith(prefix)) { 127 | return super.loadClass(name); 128 | } 129 | 130 | if (loadedClasses.contains(name) || unavaiClasses.contains(name)) { 131 | return super.loadClass(name); // Use default CL cache 132 | } 133 | 134 | byte[] newClassData = loadNewClass(name); 135 | if (newClassData != null) { 136 | loadedClasses.add(name); 137 | return loadClass(newClassData, name); 138 | } else { 139 | unavaiClasses.add(name); 140 | return parent.loadClass(name); 141 | } 142 | } 143 | 144 | public AggressiveClassLoader setParent(ClassLoader parent) { 145 | this.parent = parent; 146 | return this; 147 | } 148 | 149 | public Class load(String name) { 150 | try { 151 | return loadClass(name); 152 | } catch (ClassNotFoundException e) { 153 | throw new RuntimeException(e); 154 | } 155 | } 156 | 157 | protected abstract byte[] loadNewClass(String name); 158 | 159 | public Class loadClass(byte[] classData, String name) { 160 | Class clazz = defineClass(name, classData, 0, classData.length); 161 | if (clazz != null) { 162 | if (clazz.getPackage() == null) { 163 | definePackage(name.replaceAll("\\.\\w+$", ""), null, null, null, null, null, null, null); 164 | } 165 | resolveClass(clazz); 166 | } 167 | return clazz; 168 | } 169 | 170 | public static String toFilePath(String name) { 171 | return name.replaceAll("\\.", "/") + ".class"; 172 | } 173 | } 174 | 175 | class DynamicClassLoader extends AggressiveClassLoader { 176 | LinkedList> loaders = new LinkedList<>(); 177 | 178 | public DynamicClassLoader(String[] paths, String clsName) { 179 | super(clsName); 180 | for (String path : paths) { 181 | File file = new File(path); 182 | 183 | F1 loader = loader(file); 184 | if (loader == null) { 185 | throw new RuntimeException("Path not exists " + path); 186 | } 187 | loaders.add(loader); 188 | } 189 | } 190 | 191 | public static F1 loader(File file) { 192 | if (!file.exists()) { 193 | return null; 194 | } else if (file.isDirectory()) { 195 | return dirLoader(file); 196 | } else { 197 | try { 198 | final JarFile jarFile = new JarFile(file); 199 | 200 | return jarLoader(jarFile); 201 | } catch (IOException e) { 202 | throw new RuntimeException(e); 203 | } 204 | } 205 | } 206 | 207 | private static File findFile(String filePath, File classPath) { 208 | File file = new File(classPath, filePath); 209 | return file.exists() ? file : null; 210 | } 211 | 212 | public static byte[] readFileToBytes(File fileToRead) { 213 | try { 214 | return IOUtil.readData(new FileInputStream(fileToRead)); 215 | } catch (IOException e) { 216 | throw new RuntimeException(e); 217 | } 218 | } 219 | 220 | public static F1 dirLoader(final File dir) { 221 | return filePath -> { 222 | File file = findFile(filePath, dir); 223 | if (file == null) { 224 | return null; 225 | } 226 | 227 | return readFileToBytes(file); 228 | }; 229 | } 230 | 231 | private static F1 jarLoader(final JarFile jarFile) { 232 | return new F1() { 233 | public byte[] e(String filePath) { 234 | ZipEntry entry = jarFile.getJarEntry(filePath); 235 | if (entry == null) { 236 | return null; 237 | } 238 | try { 239 | return IOUtil.readData(jarFile.getInputStream(entry)); 240 | } catch (IOException e) { 241 | throw new RuntimeException(e); 242 | } 243 | } 244 | 245 | @Override 246 | protected void finalize() throws Throwable { 247 | IOUtil.close(jarFile); 248 | super.finalize(); 249 | } 250 | }; 251 | } 252 | 253 | @Override 254 | protected byte[] loadNewClass(String name) { 255 | System.out.println("Loading class " + name); 256 | for (F1 loader : loaders) { 257 | byte[] data = loader.e(AggressiveClassLoader.toFilePath(name)); 258 | if (data!= null) { 259 | return data; 260 | } 261 | } 262 | return null; 263 | } 264 | } 265 | 266 | public class Loader 267 | { 268 | public static int init(String cfg, long tq) { 269 | try { 270 | var cfgItems = cfg.split(","); 271 | var clsName = cfgItems[0]; 272 | clsName = clsName.substring(0, clsName.length() - 1); 273 | String classpath = System.getenv("CLASSPATH"); 274 | var paths = classpath.split(":"); 275 | 276 | var tmp = clsName.split("\\."); 277 | var prefix = String.join(".", Arrays.copyOfRange(tmp, 0, tmp.length-1)); 278 | 279 | Class cls = new DynamicClassLoader(paths, prefix).load(clsName); 280 | var method = cls.getDeclaredMethod(cfgItems[1], String.class, long.class); 281 | Object ret = method.invoke(null, cfgItems.length > 2 ? cfgItems[2] : "", tq); 282 | int rc = ((Integer)ret).intValue(); 283 | System.out.println(rc); 284 | return rc; 285 | } catch (Exception e) { 286 | e.printStackTrace(); 287 | } 288 | return 1; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /examples/java/nginx.conf: -------------------------------------------------------------------------------- 1 | daemon off; 2 | error_log /dev/stderr info; 3 | worker_processes auto; 4 | env LD_LIBRARY_PATH; 5 | env CLASSPATH; 6 | env JAVA_OPTS; 7 | 8 | events {} 9 | 10 | http { 11 | server { 12 | listen 20000; 13 | 14 | location /echo { 15 | content_by_lua_block { 16 | require("resty_ffi") 17 | local demo = ngx.load_ffi("ffi_java", "demo.echo.App?,init,") 18 | local ok, res = demo:echo("hello") 19 | assert(ok) 20 | assert(res == "hello") 21 | 22 | demo:__unload() 23 | ok, res = demo:echo("foobar") 24 | assert(not ok) 25 | ngx.log(ngx.ERR, res) 26 | 27 | ngx.say("ok") 28 | } 29 | } 30 | 31 | location /http2 { 32 | content_by_lua_block { 33 | require("resty_ffi") 34 | local demo = ngx.load_ffi("ffi_java", "demo.http2.App?,init,") 35 | local ok, res = demo:send([[ {"method": "GET", "uri": "https://httpbin.org/anything/get", "body": "foobar"} ]]) 36 | assert(ok) 37 | local cjson = require("cjson") 38 | res = cjson.decode(res) 39 | for k, v in pairs(res.headers) do 40 | ngx.say(k, ": ", v) 41 | end 42 | ngx.say(res.body) 43 | demo:__unload() 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/java/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | set -x 4 | 5 | NGINX_BIN=${NGINX_BIN:-/opt/resty_ffi/nginx/sbin/nginx} 6 | 7 | . /etc/os-release 8 | 9 | [[ -d logs ]] || mkdir logs 10 | 11 | export CLASSPATH=${PWD}/http2-demo/target/http2-demo-1.0-SNAPSHOT-jar-with-dependencies.jar 12 | CLASSPATH+=:${PWD}/echo-demo/target/echo-demo-1.0-SNAPSHOT-jar-with-dependencies.jar 13 | CLASSPATH+=:${PWD}/loader/target/resty-ffi-loader-1.0-SNAPSHOT-jar-with-dependencies.jar 14 | 15 | if [[ $ID == "centos" ]]; then 16 | export LD_LIBRARY_PATH=/usr/lib/jvm/java-11-openjdk/lib/server:/usr/lib/jvm/java-11-openjdk/lib:/usr/local/lib/lua/5.1:${PWD} 17 | else 18 | export LD_LIBRARY_PATH=/usr/lib/jvm/java-11-openjdk-amd64/lib/server:/usr/lib/jvm/java-11-openjdk-amd64/lib:/usr/local/lib/lua/5.1:${PWD} 19 | fi 20 | 21 | ${NGINX_BIN} -p ${PWD} -c nginx.conf 22 | -------------------------------------------------------------------------------- /examples/nodejs/.gitignore: -------------------------------------------------------------------------------- 1 | *_temp/ 2 | *.so 3 | *.h 4 | logs 5 | build/ 6 | -------------------------------------------------------------------------------- /examples/nodejs/Makefile: -------------------------------------------------------------------------------- 1 | NGINX_BIN ?= /opt/resty_ffi/nginx/sbin/nginx 2 | 3 | .PHONY: build 4 | build: 5 | @bash build.sh 6 | 7 | .PHONY: run 8 | run: 9 | @bash -c '[[ -d logs ]] || mkdir logs' 10 | LD_LIBRARY_PATH=$(PWD):/usr/local/lib/:/usr/local/lib/lua/5.1 NODE_PATH=$(PWD):$(PWD)/build/Release $(NGINX_BIN) -p $(PWD) -c nginx.conf 11 | 12 | .PHONY: test 13 | test: 14 | curl localhost:20000/echo 15 | curl localhost:20000/http2 16 | -------------------------------------------------------------------------------- /examples/nodejs/README.md: -------------------------------------------------------------------------------- 1 | # Node.js ffi library 2 | 3 | * `demo/echo.py` 4 | 5 | Echo the request. 6 | 7 | * `demo/http2.py` 8 | 9 | Simple http2 client. 10 | 11 | ## Background 12 | 13 | Check my blog for detail: 14 | 15 | http://luajit.io/post/use-nodejs-to-extend-openresty/ 16 | 17 | ## Build and install Node.js 18 | 19 | Node.js doesn't provide shared library officially, 20 | so we need to build it from source. 21 | 22 | Note that we need to use `--shared-openssl` option to use system openssl, 23 | otherwise the embedding into openresty would crash on openssl APIs 24 | (the reason is unknown yet). 25 | 26 | And, by default, Node.js builds a lot of libraries with static linking, 27 | so the build consumes a long time, then it's better to run `make -j` where 28 | `` is the number of CPU cores to speed up the build process. 29 | 30 | Below steps build and install Node.js into `/usr/local`. 31 | 32 | ```bash 33 | apt install libssl-dev 34 | 35 | cd /opt 36 | git clone https://github.com/nodejs/node 37 | cd /opt/node 38 | 39 | git checkout v19.2.0 40 | 41 | ./configure --shared --shared-openssl --debug-node --gdb 42 | make -j2 install 43 | ln -sf libnode.so.111 /usr/local/lib/libnode.so 44 | ``` 45 | 46 | ## Build and test 47 | 48 | ```bash 49 | # compile loader library 50 | make 51 | 52 | # run nginx 53 | make run 54 | # or specify nginx executable file path 55 | # make run NGINX_BIN=/path/to/nginx 56 | 57 | # in another terminal 58 | # curl the tests 59 | make test 60 | ``` 61 | 62 | ## Library Skelton 63 | 64 | ```js 65 | const { Worker, isMainThread, parentPort } = require('node:worker_threads'); 66 | const ffi = require("resty_ffi") 67 | 68 | if (isMainThread) { 69 | // main thread logic 70 | // define a module method to do init 71 | exports.init = (cfg, tq) => { 72 | // create a worker thread 73 | // note that the worker thread also requires this module 74 | var worker = new Worker(__filename); 75 | // get a new task from nginx 76 | worker.on('message', (tsk) => { 77 | const req = ffi.get_req(tsk) 78 | // 79 | // handle request here... 80 | // 81 | ffi.respond(tsk, 0, req) 82 | }); 83 | // tell the worker thread to do polling 84 | worker.postMessage(tq); 85 | return 0; 86 | }; 87 | } else { 88 | // worker thread logic 89 | parentPort.once('message', (tq) => { 90 | while (true) { 91 | // poll task from nginx 92 | const tsk = ffi.poll_task(tq) 93 | // if task queue is done, i.e. __unload() by lua, then exit 94 | if (tsk == 0) { 95 | console.log('exit ffi nodejs echo runtime'); 96 | break; 97 | } 98 | // notify the main thread to handle the request 99 | parentPort.postMessage(tsk); 100 | } 101 | }); 102 | } 103 | ``` 104 | 105 | Specify the entry module and function in lua and use it: 106 | 107 | ```lua 108 | -- note that the nodejs loader library symbols must be opened in global 109 | local demo = ngx.load_ffi("ffi_nodejs", "demo/echo,init", {is_global=true}) 110 | local ok, res = demo:echo("hello") 111 | assert(ok) 112 | assert(res == "hello") 113 | 114 | -- You could use '?' as module suffix to indicate that the module is hot-reload. 115 | -- When the module gets loaded next time (after previous unload), 116 | -- it would reload the module instead of the VM cached version 117 | local demo = ngx.load_ffi("ffi_nodejs", "demo/echo?,init", {is_global=true}) 118 | ``` 119 | -------------------------------------------------------------------------------- /examples/nodejs/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "resty_ffi", 5 | "sources": [ 6 | "resty_ffi.cc", 7 | ] 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/nodejs/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | set -x 4 | 5 | . /etc/os-release 6 | 7 | if [[ $ID == "centos" ]]; then 8 | set +eu 9 | if [[ $VERSION_ID == "7" ]]; then 10 | source scl_source enable devtoolset-9 11 | elif [[ $VERSION_ID == "8" ]]; then 12 | source /opt/rh/gcc-toolset-9/enable 13 | fi 14 | set -eu 15 | fi 16 | 17 | g++ -std=c++17 -pthread -I/usr/local/include/node/ \ 18 | -fPIC -shared -lnode -L/usr/local/lib -pthread \ 19 | -o libffi_nodejs.so loader.cc 20 | 21 | node-gyp configure build 22 | -------------------------------------------------------------------------------- /examples/nodejs/demo/echo.js: -------------------------------------------------------------------------------- 1 | const { Worker, isMainThread, parentPort } = require('node:worker_threads'); 2 | const ffi = require("resty_ffi") 3 | 4 | if (isMainThread) { 5 | exports.init = (cfg, tq) => { 6 | var worker = new Worker(__filename); 7 | worker.on('message', (tsk) => { 8 | const req = ffi.get_req(tsk) 9 | ffi.respond(tsk, 0, req) 10 | }); 11 | worker.postMessage(tq); 12 | return 0; 13 | }; 14 | } else { 15 | parentPort.once('message', (tq) => { 16 | while (true) { 17 | const tsk = ffi.poll_task(tq) 18 | if (tsk == 0) { 19 | console.log('exit ffi nodejs echo runtime'); 20 | break; 21 | } 22 | parentPort.postMessage(tsk); 23 | } 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /examples/nodejs/demo/http2.js: -------------------------------------------------------------------------------- 1 | const { Worker, isMainThread, parentPort } = require('node:worker_threads'); 2 | const ffi = require("resty_ffi") 3 | const http2 = require('node:http2'); 4 | 5 | if (isMainThread) { 6 | exports.init = (cfg, tq) => { 7 | var worker = new Worker(__filename); 8 | worker.on('message', (tsk) => { 9 | var req_data = ffi.get_req(tsk) 10 | req_data = JSON.parse(req_data) 11 | 12 | const client = http2.connect(req_data.host); 13 | client.on('error', (err) => { 14 | console.error(err) 15 | }); 16 | 17 | var rsp = {'headers': {}} 18 | const req = client.request({ ':path': req_data.path }); 19 | req.on('response', (headers, flags) => { 20 | for (const name in headers) { 21 | rsp['headers'][name] = headers[name] 22 | } 23 | }); 24 | req.setEncoding('utf8'); 25 | let data = ''; 26 | req.on('data', (chunk) => { data += chunk; }); 27 | req.on('end', () => { 28 | rsp['body'] = data 29 | ffi.respond(tsk, 0, JSON.stringify(rsp)) 30 | client.close(); 31 | }); 32 | req.end(); 33 | }); 34 | worker.postMessage(tq); 35 | return 0; 36 | }; 37 | } else { 38 | parentPort.once('message', (tq) => { 39 | while (true) { 40 | const tsk = ffi.poll_task(tq) 41 | if (tsk == 0) { 42 | console.log('exit ffi nodejs http2 runtime'); 43 | break; 44 | } 45 | parentPort.postMessage(tsk); 46 | } 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /examples/nodejs/loader.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Jinhua Luo (kingluo) luajit.io@gmail.com 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its 16 | * contributors may be used to endorse or promote products derived from 17 | * this software without specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | #include "node.h" 32 | #include "uv.h" 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | 39 | using node::CommonEnvironmentSetup; 40 | using node::Environment; 41 | using node::MultiIsolatePlatform; 42 | using v8::Context; 43 | using v8::HandleScope; 44 | using v8::Isolate; 45 | using v8::Locker; 46 | using v8::MaybeLocal; 47 | using v8::V8; 48 | using v8::Value; 49 | 50 | static int client_fd; 51 | static bool initialized = false; 52 | static pthread_mutex_t init_lock = PTHREAD_MUTEX_INITIALIZER; 53 | 54 | static void* thread_fn(void* data) 55 | { 56 | int argc = 1; 57 | char _null[1]; 58 | char* argvv = _null; 59 | char** argv = &argvv; 60 | argv = uv_setup_args(argc, argv); 61 | std::vector args(argv, argv + argc); 62 | std::unique_ptr result = 63 | node::InitializeOncePerProcess(args, { 64 | node::ProcessInitializationFlags::kNoInitializeV8, 65 | node::ProcessInitializationFlags::kNoInitializeNodeV8Platform 66 | }); 67 | 68 | for (const std::string& error : result->errors()) 69 | fprintf(stderr, "%s: %s\n", args[0].c_str(), error.c_str()); 70 | if (result->early_return() != 0) { 71 | fprintf(stderr, "early return: exit_code=%d\n", result->exit_code()); 72 | return NULL; 73 | } 74 | 75 | std::unique_ptr platform = 76 | MultiIsolatePlatform::Create(4); 77 | V8::InitializePlatform(platform.get()); 78 | V8::Initialize(); 79 | 80 | std::vector errors; 81 | std::unique_ptr setup = 82 | CommonEnvironmentSetup::Create(platform.get(), &errors, result->args(), result->exec_args()); 83 | if (!setup) { 84 | for (const std::string& err : errors) 85 | fprintf(stderr, "%s: %s\n", args[0].c_str(), err.c_str()); 86 | return NULL; 87 | } 88 | 89 | Isolate* isolate = setup->isolate(); 90 | Environment* env = setup->env(); 91 | 92 | { 93 | Locker locker(isolate); 94 | Isolate::Scope isolate_scope(isolate); 95 | HandleScope handle_scope(isolate); 96 | Context::Scope context_scope(setup->context()); 97 | 98 | MaybeLocal loadenv_ret = node::LoadEnvironment( 99 | env, 100 | "const publicRequire =" 101 | " require('module').createRequire(process.env.NODE_PATH + '/');" 102 | "globalThis.require = publicRequire;" 103 | "require('vm').runInThisContext(\"" 104 | "const net = require('net');" 105 | "const fs = require('fs');" 106 | "const path = '/tmp/.resty_ffi_nodejs_loader.' + process.pid;" 107 | "try { fs.unlinkSync(path) } catch(err) {}" 108 | "const unixSocketServer = net.createServer();" 109 | "unixSocketServer.listen(path, () => {" 110 | " console.log('ffi nodejs listening');" 111 | "});" 112 | "unixSocketServer.on('connection', (s) => {" 113 | " s.on('data', (data) => {" 114 | " try {" 115 | " const arr = data.toString().split(',');" 116 | " const tq = BigInt(arr[0]);" 117 | " var mod = arr[1];" 118 | " const func = arr[2];" 119 | " const cfg = '';" 120 | " if (arr.length == 4) {" 121 | " cfg = arr[3];" 122 | " }" 123 | " if (mod.endsWith('?')) {" 124 | " mod = mod.slice(0, -1);" 125 | " var mp = require.resolve(mod);" 126 | " if (require.cache[mp]) {" 127 | " delete require.cache[mp];" 128 | " console.log(`[clear] module: ${mp}`);" 129 | " }" 130 | " }" 131 | " var rc = require(mod)[func](cfg, tq);" 132 | " s.write(rc.toString());" 133 | " } catch(err) {" 134 | " console.log(err);" 135 | " s.write('1');" 136 | " return;" 137 | " }" 138 | " });" 139 | "});" 140 | "\");" 141 | ); 142 | 143 | if (loadenv_ret.IsEmpty()) // There has been a JS exception. 144 | return NULL; 145 | 146 | node::SpinEventLoop(env).FromMaybe(1); 147 | 148 | node::Stop(env); 149 | } 150 | 151 | V8::Dispose(); 152 | V8::DisposePlatform(); 153 | 154 | node::TearDownOncePerProcess(); 155 | 156 | return NULL; 157 | } 158 | 159 | extern "C" int libffi_init(char* cfg, void *tq) 160 | { 161 | pthread_mutex_lock(&init_lock); 162 | 163 | if (!initialized) { 164 | initialized = true; 165 | 166 | pthread_attr_t attr; 167 | pthread_attr_init(&attr); 168 | pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 169 | 170 | pthread_t tid; 171 | pthread_create(&tid, &attr, thread_fn, NULL); 172 | sleep(1); 173 | 174 | // connect nodejs 175 | if ((client_fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) { 176 | perror("socket"); 177 | } 178 | struct sockaddr_un addr; 179 | memset(&addr, 0, sizeof(addr)); 180 | addr.sun_family = AF_UNIX; 181 | snprintf(addr.sun_path, UNIX_PATH_MAX, "/tmp/.resty_ffi_nodejs_loader.%d", getpid()); 182 | for (int i = 0; i < 10; i++) { 183 | if (connect(client_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { 184 | perror("connect"); 185 | sleep(1); 186 | } else { 187 | break; 188 | } 189 | } 190 | } 191 | 192 | int ret = 0; 193 | char buf[8192]; 194 | memset(buf, 0, 8192); 195 | sprintf(buf, "%p,%s", tq, cfg); 196 | if (send(client_fd, buf, strlen(buf), 0) == -1) { 197 | perror("send"); 198 | } 199 | 200 | int len; 201 | memset(buf, 0, 8192); 202 | if ((len = recv(client_fd, buf, 8192, 0)) < 0) { 203 | perror("recv"); 204 | } 205 | sscanf(buf, "%d", &ret); 206 | 207 | pthread_mutex_unlock(&init_lock); 208 | return ret; 209 | } 210 | -------------------------------------------------------------------------------- /examples/nodejs/nginx.conf: -------------------------------------------------------------------------------- 1 | daemon off; 2 | error_log /dev/stderr info; 3 | worker_processes auto; 4 | env LD_LIBRARY_PATH; 5 | env NODE_PATH; 6 | 7 | events {} 8 | 9 | http { 10 | server { 11 | listen 20000; 12 | 13 | location /echo { 14 | content_by_lua_block { 15 | require("resty_ffi") 16 | local demo = ngx.load_ffi("ffi_nodejs", "demo/echo,init", {is_global=true}) 17 | local ok, res = demo:echo("hello") 18 | assert(ok) 19 | assert(res == "hello") 20 | demo:__unload() 21 | ngx.say("ok") 22 | } 23 | } 24 | 25 | location /http2 { 26 | content_by_lua_block { 27 | require("resty_ffi") 28 | local demo = ngx.load_ffi("ffi_nodejs", "demo/http2?,init", {is_global=true}) 29 | local ok, res = demo:get([[{"host": "https://httpbin.org", "path": "/anything"}]]) 30 | assert(ok) 31 | ngx.say(res) 32 | demo:__unload() 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/nodejs/resty_ffi.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Jinhua Luo (kingluo) luajit.io@gmail.com 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its 16 | * contributors may be used to endorse or promote products derived from 17 | * this software without specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | #include 32 | #include 33 | 34 | extern "C" { 35 | void* ngx_http_lua_ffi_task_poll(void *p); 36 | char* ngx_http_lua_ffi_get_req(void *tsk, int *len); 37 | void ngx_http_lua_ffi_respond(void *tsk, int rc, char* rsp, int rsp_len); 38 | } 39 | 40 | namespace resty_ffi { 41 | napi_value poll_task(napi_env env, napi_callback_info args) { 42 | size_t argc = 1; 43 | napi_value argv[1]; 44 | napi_status status = napi_get_cb_info(env, args, &argc, argv, NULL, NULL); 45 | if (status != napi_ok || argc != 1) { 46 | napi_throw_type_error(env, NULL, "Wrong number of arguments."); 47 | return NULL; 48 | } 49 | 50 | uint64_t ptr = 0; 51 | bool lossless; 52 | napi_get_value_bigint_uint64(env, argv[0], &ptr, &lossless); 53 | (void)lossless; 54 | 55 | void* tq = (void*)ptr; 56 | uint64_t tsk = (uint64_t)ngx_http_lua_ffi_task_poll(tq); 57 | 58 | napi_value ret; 59 | napi_create_bigint_uint64(env, tsk, &ret); 60 | return ret; 61 | } 62 | 63 | napi_value get_req(napi_env env, napi_callback_info args) { 64 | size_t argc = 1; 65 | napi_value argv[1]; 66 | napi_status status = napi_get_cb_info(env, args, &argc, argv, NULL, NULL); 67 | if (status != napi_ok || argc != 1) { 68 | napi_throw_type_error(env, NULL, "Wrong number of arguments."); 69 | return NULL; 70 | } 71 | 72 | uint64_t ptr = 0; 73 | bool lossless; 74 | napi_get_value_bigint_uint64(env, argv[0], &ptr, &lossless); 75 | (void)lossless; 76 | 77 | void* tsk = (void*)ptr; 78 | int len; 79 | char* req = ngx_http_lua_ffi_get_req(tsk, &len); 80 | if (req) { 81 | napi_value ret; 82 | status = napi_create_string_utf8(env, req, len, &ret); 83 | if (status != napi_ok) return nullptr; 84 | return ret; 85 | } 86 | 87 | return nullptr; 88 | } 89 | 90 | napi_value respond(napi_env env, napi_callback_info args) { 91 | size_t argc = 3; 92 | napi_value argv[3]; 93 | napi_status status = napi_get_cb_info(env, args, &argc, argv, NULL, NULL); 94 | if (status != napi_ok || (argc != 2 && argc != 3)) { 95 | napi_throw_type_error(env, NULL, "Wrong number of arguments."); 96 | return NULL; 97 | } 98 | 99 | uint64_t ptr = 0; 100 | bool lossless; 101 | napi_get_value_bigint_uint64(env, argv[0], &ptr, &lossless); 102 | (void)lossless; 103 | void* tsk = (void*)ptr; 104 | 105 | int32_t rc = 0; 106 | napi_get_value_int32(env, argv[1], &rc); 107 | 108 | char* buf = NULL; 109 | int len = 0; 110 | if (argc == 3) { 111 | size_t str_size; 112 | size_t len; 113 | napi_get_value_string_utf8(env, argv[2], NULL, 0, &str_size); 114 | buf = (char*)calloc(str_size + 1, sizeof(char)); 115 | str_size = str_size + 1; 116 | napi_get_value_string_utf8(env, argv[2], buf, str_size, &len); 117 | } 118 | 119 | ngx_http_lua_ffi_respond(tsk, rc, buf, len); 120 | 121 | return nullptr; 122 | } 123 | 124 | napi_value init(napi_env env, napi_value exports) { 125 | napi_status status; 126 | napi_value fn; 127 | 128 | status = napi_create_function(env, nullptr, 0, poll_task, nullptr, &fn); 129 | if (status != napi_ok) return nullptr; 130 | 131 | status = napi_set_named_property(env, exports, "poll_task", fn); 132 | if (status != napi_ok) return nullptr; 133 | 134 | status = napi_create_function(env, nullptr, 0, get_req, nullptr, &fn); 135 | if (status != napi_ok) return nullptr; 136 | 137 | status = napi_set_named_property(env, exports, "get_req", fn); 138 | if (status != napi_ok) return nullptr; 139 | 140 | status = napi_create_function(env, nullptr, 0, respond, nullptr, &fn); 141 | if (status != napi_ok) return nullptr; 142 | 143 | status = napi_set_named_property(env, exports, "respond", fn); 144 | if (status != napi_ok) return nullptr; 145 | 146 | return exports; 147 | } 148 | 149 | NAPI_MODULE(NODE_GYP_MODULE_NAME, init) 150 | } // namespace resty_ffi 151 | -------------------------------------------------------------------------------- /examples/python/.gitignore: -------------------------------------------------------------------------------- 1 | *_temp/ 2 | __pycache__ 3 | *.so 4 | *.h 5 | logs 6 | -------------------------------------------------------------------------------- /examples/python/Makefile: -------------------------------------------------------------------------------- 1 | NGINX_BIN ?= /opt/resty_ffi/nginx/sbin/nginx 2 | 3 | .PHONY: build 4 | build: 5 | @bash build.sh 6 | 7 | .PHONY: run 8 | run: 9 | pip3 install -r requirements.txt 10 | @bash -c '[[ -d logs ]] || mkdir logs' 11 | LD_LIBRARY_PATH=$(PWD):/usr/local/lib/lua/5.1 PYTHONPATH=$(PWD) $(NGINX_BIN) -p $(PWD) -c nginx.conf 12 | 13 | .PHONY: test 14 | test: 15 | curl localhost:20000/echo 16 | curl localhost:20000/kafka 17 | -------------------------------------------------------------------------------- /examples/python/README.md: -------------------------------------------------------------------------------- 1 | # Python loader library 2 | 3 | It uses [`cffi`](https://cffi.readthedocs.io/) to call lua-resty-ffi C APIs. 4 | 5 | ## Examples 6 | 7 | * `ffi/echo.py` Minimal example, echo the request. 8 | * `ffi/kafka.py` Simple kafka client. Implements produce and consumer group. 9 | * [lua-resty-ffi-ldap](https://github.com/kingluo/lua-resty-ffi-ldap) ldap client, supports all SASL auth methods 10 | * [lua-resty-ffi-graphql-resolver](https://github.com/kingluo/lua-resty-ffi-graphql-resolver) embed graphql server into nginx 11 | * [lua-resty-ffi-soap](https://github.com/kingluo/lua-resty-ffi-soap) The openresty SOAP to REST library based on zeep. 12 | 13 | ## Install via luarocks 14 | 15 | ```bash 16 | # install lua-resty-ffi and lua-resty-ffi-python 17 | # https://github.com/kingluo/lua-resty-ffi#install-lua-resty-ffi-via-luarocks 18 | # set `OR_SRC` to your openresty source path 19 | luarocks config variables.OR_SRC /tmp/tmp.Z2UhJbO1Si/openresty-1.21.4.1 20 | luarocks install lua-resty-ffi-python 21 | ``` 22 | 23 | ## Build from source 24 | 25 | ```bash 26 | # compile loader library 27 | make 28 | ``` 29 | 30 | ## Demo 31 | 32 | ```bash 33 | # install latest docker version 34 | # https://docs.docker.com/engine/install/ 35 | 36 | # start kafka container for test 37 | docker compose up -d 38 | 39 | # ensure kafka is ready 40 | docker compose logs -f 41 | 42 | # run nginx 43 | make run 44 | # or specify nginx executable file path 45 | # make run NGINX_BIN=/path/to/nginx 46 | 47 | # in another terminal 48 | # curl the tests 49 | make test 50 | ``` 51 | 52 | ## Library Skelton 53 | 54 | ```python 55 | class State: 56 | def poll(self, tq): 57 | while True: 58 | # poll a task 59 | task = C.ngx_http_lua_ffi_task_poll(ffi.cast("void*", tq)) 60 | # if the task queue is done, then quit 61 | if task == ffi.NULL: 62 | break 63 | # get the request from the task 64 | # It assumes the message in C string, so no need to get the request length 65 | req = C.ngx_http_lua_ffi_get_req(task, ffi.NULL) 66 | # copy the request as response, allocated by C strdup() 67 | # note that both request and response would be freed by nginx 68 | res = C.strdup(req) 69 | C.ngx_http_lua_ffi_respond(task, 0, res, 0) 70 | print("exit python echo runtime") 71 | 72 | # implement an entry function 73 | def init(cfg, tq): 74 | # launch a thread to poll tasks 75 | st = State() 76 | t = threading.Thread(target=st.poll, args=(tq,)) 77 | t.daemon = True 78 | t.start() 79 | return 0 80 | ``` 81 | 82 | Specify the entry module and function in lua and use it: 83 | 84 | ```lua 85 | -- note that the python loader library symbols must be opened in global 86 | local demo = ngx.load_ffi("resty_ffi_python", "ffi.echo,init", 87 | {is_global = true, unpin = true}) 88 | local ok, res = demo:echo("hello") 89 | assert(ok) 90 | assert(res == "hello") 91 | 92 | -- for python >= 3.8, you could use '?' as module suffix 93 | -- to indicate that the module is hot-reload, i.e. 94 | -- when the module gets loaded next time (after previous unload), 95 | -- it would reload the module instead of the VM cached version 96 | local demo = ngx.load_ffi("resty_ffi_python", "ffi.echo?,init", 97 | {is_global = true, unpin = true}) 98 | ``` 99 | -------------------------------------------------------------------------------- /examples/python/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | set -x 4 | 5 | . /etc/os-release 6 | 7 | prepare_centos() { 8 | if [[ $(rpm --eval '%{centos_ver}') == "7" ]]; then 9 | yum -y install centos-release-scl 10 | yum -y install devtoolset-9 patch wget git make sudo 11 | set +eu 12 | source scl_source enable devtoolset-9 13 | set -eu 14 | elif [[ $(rpm --eval '%{centos_ver}') == "8" ]]; then 15 | dnf install -y gcc-toolset-9-toolchain patch wget git make sudo 16 | dnf install -y yum-utils 17 | set +eu 18 | source /opt/rh/gcc-toolset-9/enable 19 | set -eu 20 | fi 21 | } 22 | 23 | if [[ $ID == "centos" ]]; then 24 | prepare_centos 25 | yum -y install python3-devel 26 | gcc $(python3-config --cflags) $(python3-config --ldflags) -o libresty_ffi_python.so loader.c \ 27 | -fPIC -shared -pthread 28 | else 29 | apt -y install build-essential 30 | apt -y install python3-dev python3-pip libffi-dev 31 | if [[ $ID == "ubuntu" ]]; then 32 | gcc $(python3-config --cflags) $(python3-config --ldflags) -o libresty_ffi_python.so loader.c \ 33 | -lpython$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))') \ 34 | -fPIC -shared -pthread 35 | elif [[ $ID == "debian" ]]; then 36 | gcc $(python3-config --cflags) $(python3-config --ldflags) -o libresty_ffi_python.so loader.c \ 37 | -fPIC -shared -pthread 38 | else 39 | echo "$ID not supported" 40 | exit 1 41 | fi 42 | fi 43 | -------------------------------------------------------------------------------- /examples/python/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | zookeeper: 5 | image: docker.io/bitnami/zookeeper:3.8 6 | ports: 7 | - "2181:2181" 8 | volumes: 9 | - "zookeeper_data:/bitnami" 10 | environment: 11 | - ALLOW_ANONYMOUS_LOGIN=yes 12 | kafka: 13 | image: docker.io/bitnami/kafka:3.2 14 | ports: 15 | - "9092:9092" 16 | volumes: 17 | - "kafka_data:/bitnami" 18 | environment: 19 | - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 20 | - ALLOW_PLAINTEXT_LISTENER=yes 21 | - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true 22 | - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 23 | - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092 24 | depends_on: 25 | - zookeeper 26 | 27 | volumes: 28 | zookeeper_data: 29 | driver: local 30 | kafka_data: 31 | driver: local 32 | -------------------------------------------------------------------------------- /examples/python/ffi/echo.py: -------------------------------------------------------------------------------- 1 | from cffi import FFI 2 | ffi = FFI() 3 | ffi.cdef(""" 4 | char *strdup(const char *s); 5 | void* ngx_http_lua_ffi_task_poll(void *p); 6 | char* ngx_http_lua_ffi_get_req(void *tsk, int *len); 7 | void ngx_http_lua_ffi_respond(void *tsk, int rc, char* rsp, int rsp_len); 8 | """) 9 | C = ffi.dlopen(None) 10 | 11 | import threading 12 | 13 | class State: 14 | def poll(self, tq): 15 | while True: 16 | task = C.ngx_http_lua_ffi_task_poll(ffi.cast("void*", tq)) 17 | if task == ffi.NULL: 18 | break 19 | req = C.ngx_http_lua_ffi_get_req(task, ffi.NULL) 20 | res = C.strdup(req) 21 | C.ngx_http_lua_ffi_respond(task, 0, res, 0) 22 | print("exit python echo runtime") 23 | 24 | def init(cfg, tq): 25 | st = State() 26 | t = threading.Thread(target=st.poll, args=(tq,)) 27 | t.daemon = True 28 | t.start() 29 | return 0 30 | -------------------------------------------------------------------------------- /examples/python/ffi/kafka.py: -------------------------------------------------------------------------------- 1 | from cffi import FFI 2 | ffi = FFI() 3 | ffi.cdef(""" 4 | void* malloc(size_t); 5 | void *memcpy(void *dest, const void *src, size_t n); 6 | void* ngx_http_lua_ffi_task_poll(void *p); 7 | char* ngx_http_lua_ffi_get_req(void *tsk, int *len); 8 | void ngx_http_lua_ffi_respond(void *tsk, int rc, char* rsp, int rsp_len); 9 | """) 10 | C = ffi.dlopen(None) 11 | 12 | import threading 13 | import json 14 | import queue 15 | import time 16 | from kafka import KafkaProducer 17 | from kafka import KafkaConsumer 18 | from kafka import KafkaAdminClient 19 | from kafka.admin import NewTopic 20 | from kafka.errors import TopicAlreadyExistsError 21 | 22 | class State: 23 | def __init__(self, cfg): 24 | self.topic = cfg["topic"] 25 | servers = cfg["servers"] 26 | 27 | self.taskq = queue.Queue() 28 | self.consume_taskq = queue.Queue() 29 | 30 | self.producer = KafkaProducer(bootstrap_servers=servers) 31 | self.consumer = KafkaConsumer(bootstrap_servers = servers, group_id = cfg["group_id"], enable_auto_commit = True) 32 | self.consumer.subscribe([self.topic]) 33 | 34 | try: 35 | admin = KafkaAdminClient(bootstrap_servers=servers) 36 | topic_list = [] 37 | topic_list.append(NewTopic(name=self.topic, num_partitions=1, replication_factor=1)) 38 | print(admin.create_topics(new_topics=topic_list, validate_only=False)) 39 | msgs = self.consumer.poll() 40 | assert not msgs 41 | except TopicAlreadyExistsError as err: 42 | pass 43 | 44 | t = threading.Thread(target=self.consumer_loop) 45 | t.daemon = True 46 | t.start() 47 | self.consumer_thread = t 48 | 49 | t = threading.Thread(target=self.producer_loop) 50 | t.daemon = True 51 | t.start() 52 | self.task_loop_thread = t 53 | 54 | def respond(self, task, data): 55 | res = C.malloc(len(data)) 56 | C.memcpy(res, data.encode(), len(data)) 57 | C.ngx_http_lua_ffi_respond(task, 0, res, len(data)) 58 | 59 | def consumer_loop(self): 60 | while True: 61 | task, _ = self.consume_taskq.get() 62 | msg = next(self.consumer) 63 | ev = { 64 | "topic": msg.topic, 65 | "partition": msg.partition, 66 | "timestamp": msg.timestamp, 67 | "key": msg.key, 68 | "value": msg.value.decode(), 69 | } 70 | data = json.dumps(ev) 71 | self.respond(task, data) 72 | self.consume_taskq.task_done() 73 | 74 | def producer_loop(self): 75 | while True: 76 | task, req = self.taskq.get() 77 | msg = req["msg"] 78 | future = self.producer.send(self.topic, msg.encode()) 79 | result = future.get(timeout=60) 80 | print(result, msg) 81 | C.ngx_http_lua_ffi_respond(task, 0, ffi.NULL, 0) 82 | self.taskq.task_done() 83 | 84 | def poll(self, tq): 85 | while True: 86 | task = C.ngx_http_lua_ffi_task_poll(ffi.cast("void*", tq)) 87 | r = C.ngx_http_lua_ffi_get_req(task, ffi.NULL) 88 | req = json.loads(ffi.string(r)) 89 | typ = req["type"] 90 | if typ == "produce": 91 | self.taskq.put((task, req)) 92 | elif typ == "consume": 93 | self.consume_taskq.put((task, req)) 94 | else: 95 | C.ngx_http_lua_ffi_respond(task, -1, ffi.NULL, 0) 96 | 97 | def init(cfg, tq): 98 | data = ffi.string(ffi.cast("char*", cfg)) 99 | cfg = json.loads(data) 100 | st = State(cfg) 101 | t = threading.Thread(target=st.poll, args=(tq,)) 102 | t.daemon = True 103 | t.start() 104 | return 0 105 | -------------------------------------------------------------------------------- /examples/python/loader.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Jinhua Luo (kingluo) luajit.io@gmail.com 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its 16 | * contributors may be used to endorse or promote products derived from 17 | * this software without specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | #define PY_SSIZE_T_CLEAN 32 | #include 33 | #include 34 | #include 35 | 36 | static PyThreadState *mainThreadState = NULL; 37 | 38 | static pthread_mutex_t init_lock = PTHREAD_MUTEX_INITIALIZER; 39 | 40 | typedef struct { 41 | char* module; 42 | char* func; 43 | char* cfg; 44 | void* tq; 45 | } state_t; 46 | 47 | static int init(state_t *state) 48 | { 49 | #if PY_VERSION_HEX >= 0x03080000 50 | int reload_module = 0; 51 | #endif 52 | int rc = 1; 53 | char* module = state->module; 54 | const char* func = state->func; 55 | 56 | char* last = strrchr(module, '/'); 57 | int dirlen; 58 | char *dir = NULL; 59 | if (last != NULL) { 60 | dirlen = strlen(module) - strlen(last); 61 | dir = strndup(module, dirlen); 62 | module = last + 1; 63 | } 64 | 65 | int lastpos = strlen(module) - 1; 66 | if (module[lastpos] == '?') { 67 | module[lastpos] = 0; 68 | #if PY_VERSION_HEX >= 0x03080000 69 | reload_module = 1; 70 | #endif 71 | } 72 | 73 | PyObject *pName, *pModule, *pFunc; 74 | PyObject *pArgs, *pValue; 75 | 76 | PyGILState_STATE gstate; 77 | gstate = PyGILState_Ensure(); 78 | 79 | if (dir != NULL) { 80 | PyObject* sys = PyImport_ImportModule( "sys" ); 81 | PyObject* sys_path = PyObject_GetAttrString( sys, "path" ); 82 | PyObject* folder_path = PyUnicode_FromString( dir ); 83 | PyList_Insert( sys_path, 0, folder_path ); 84 | } 85 | 86 | pName = PyUnicode_DecodeFSDefault(module); 87 | /* Error checking of pName left out */ 88 | 89 | #if PY_VERSION_HEX >= 0x03080000 90 | pModule = PyImport_GetModule(pName); 91 | if (pModule == NULL) { 92 | #endif 93 | pModule = PyImport_Import(pName); 94 | #if PY_VERSION_HEX >= 0x03080000 95 | } else if (reload_module) { 96 | pModule = PyImport_ReloadModule(pModule); 97 | } 98 | #endif 99 | 100 | Py_DECREF(pName); 101 | 102 | if (pModule != NULL) { 103 | pFunc = PyObject_GetAttrString(pModule, func); 104 | /* pFunc is a new reference */ 105 | 106 | if (pFunc && PyCallable_Check(pFunc)) { 107 | pArgs = PyTuple_New(2); 108 | PyTuple_SetItem(pArgs, 0, PyLong_FromVoidPtr(state->cfg)); 109 | PyTuple_SetItem(pArgs, 1, PyLong_FromVoidPtr(state->tq)); 110 | 111 | pValue = PyObject_CallObject(pFunc, pArgs); 112 | Py_DECREF(pArgs); 113 | if (pValue != NULL) { 114 | rc = PyLong_AsLong(pValue); 115 | // printf("Result of call: %ld\n", PyLong_AsLong(pValue)); 116 | Py_DECREF(pValue); 117 | goto end; 118 | } else { 119 | Py_DECREF(pFunc); 120 | Py_DECREF(pModule); 121 | PyErr_Print(); 122 | fprintf(stderr,"Call failed\n"); 123 | goto end; 124 | } 125 | } else { 126 | if (PyErr_Occurred()) { 127 | PyErr_Print(); 128 | } 129 | fprintf(stderr, "Cannot find function \"%s\"\n", func); 130 | } 131 | Py_XDECREF(pFunc); 132 | Py_DECREF(pModule); 133 | } else { 134 | PyErr_Print(); 135 | fprintf(stderr, "Failed to load \"%s\"\n", module); 136 | goto end; 137 | } 138 | 139 | end: 140 | PyGILState_Release(gstate); 141 | return rc; 142 | } 143 | 144 | int libffi_init(char* cfg, void *tq) 145 | { 146 | pthread_mutex_lock(&init_lock); 147 | 148 | if (mainThreadState == NULL) { 149 | Py_Initialize(); 150 | mainThreadState = PyEval_SaveThread(); 151 | } 152 | 153 | state_t state = {0}; 154 | state.tq = tq; 155 | 156 | char* str = cfg; 157 | state.module = strsep(&str, ","); 158 | state.func = strsep(&str, ","); 159 | state.cfg = str; 160 | 161 | int rc = init(&state); 162 | 163 | pthread_mutex_unlock(&init_lock); 164 | 165 | return rc; 166 | } 167 | 168 | __attribute__((destructor)) void ffi_python_fini(void) { 169 | if (mainThreadState) { 170 | PyEval_RestoreThread(mainThreadState); 171 | Py_Finalize(); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /examples/python/nginx.conf: -------------------------------------------------------------------------------- 1 | daemon off; 2 | error_log /dev/stderr info; 3 | worker_processes auto; 4 | env LD_LIBRARY_PATH; 5 | env PYTHONPATH; 6 | 7 | events {} 8 | 9 | http { 10 | server { 11 | listen 20000; 12 | 13 | location /gc { 14 | content_by_lua_block { 15 | collectgarbage() 16 | ngx.say("ok") 17 | } 18 | } 19 | 20 | location /echo { 21 | content_by_lua_block { 22 | require("resty_ffi") 23 | local demo = ngx.load_ffi("resty_ffi_python", "ffi.echo?,init", 24 | {is_global = true, unpin = true}) 25 | local ok, res = demo:echo("hello") 26 | assert(ok) 27 | assert(res == "hello") 28 | 29 | demo:__unload() 30 | ok, res = demo:echo("foobar") 31 | assert(not ok) 32 | ngx.log(ngx.ERR, res) 33 | 34 | ngx.say("ok") 35 | } 36 | } 37 | 38 | location /kafka { 39 | content_by_lua_block { 40 | require("resty_ffi") 41 | local opts = {is_global = true} 42 | local demo = ngx.load_ffi("resty_ffi_python", 43 | [[ffi.kafka,init,{"servers":"localhost:9092", "topic":"foobar", "group_id": "foobar"}]], opts) 44 | local ok, res 45 | ok, res = demo:produce([[{"type":"produce", "msg":"hello"}]]) 46 | assert(ok) 47 | ok, res = demo:produce([[{"type":"produce", "msg":"world"}]]) 48 | assert(ok) 49 | local ok, res = demo:consume([[{"type":"consume"}]]) 50 | assert(ok) 51 | ngx.say(res) 52 | local ok, res = demo:consume([[{"type":"consume"}]]) 53 | assert(ok) 54 | ngx.say(res) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/python/requirements.txt: -------------------------------------------------------------------------------- 1 | cffi==1.15.1 2 | kafka-python==2.0.2 3 | -------------------------------------------------------------------------------- /examples/rust/.gitignore: -------------------------------------------------------------------------------- 1 | *.so 2 | logs 3 | -------------------------------------------------------------------------------- /examples/rust/Makefile: -------------------------------------------------------------------------------- 1 | NGINX_BIN ?= /opt/resty_ffi/nginx/sbin/nginx 2 | 3 | .PHONY: build 4 | build: 5 | cd echo; cargo build --release 6 | 7 | .PHONY: run 8 | run: 9 | @bash -c '[[ -d logs ]] || mkdir logs' 10 | LD_LIBRARY_PATH=$(PWD)/echo/target/release:/usr/local/lib/lua/5.1 $(NGINX_BIN) -p $(PWD) -c nginx.conf 11 | 12 | .PHONY: test 13 | test: 14 | curl localhost:20000/echo 15 | -------------------------------------------------------------------------------- /examples/rust/README.md: -------------------------------------------------------------------------------- 1 | # Rust ffi library examples 2 | 3 | * `echo.go` Minimal example, echo your request. 4 | * [lua-resty-ffi-grpc](https://github.com/kingluo/lua-resty-ffi-grpc) 5 | 6 | ## Install rust if not yet 7 | 8 | ```bash 9 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 10 | source "$HOME/.cargo/env" 11 | ``` 12 | 13 | ## Build and test 14 | 15 | ```bash 16 | # build examples libraries 17 | make 18 | 19 | # run nginx 20 | make run 21 | # or specify nginx executable file path 22 | # make run NGINX_BIN=/path/to/nginx 23 | 24 | # in another terminal 25 | # curl the tests 26 | make test 27 | ``` 28 | 29 | ## Library Skelton 30 | 31 | ```rust 32 | #[no_mangle] 33 | pub extern "C" fn libffi_init(cfg: *mut c_char, tq: *const c_void) -> c_int { 34 | let cfg = unsafe { CStr::from_ptr(cfg).to_string_lossy() }; 35 | let cfg = serde_json::from_str::(&cfg); 36 | if !cfg.is_ok() { 37 | return 1; 38 | } 39 | let cfg: Value = cfg.unwrap(); 40 | 41 | let tq = TaskQueueHandle(tq); 42 | thread::spawn(move || { 43 | println!("cfg: {:?}", cfg); 44 | let tq = tq.clone(); 45 | let nullptr = std::ptr::null_mut(); 46 | loop { 47 | unsafe { 48 | // poll a task 49 | let task = ngx_http_lua_ffi_task_poll(tq.0); 50 | // if the task queue is done, then quit 51 | if task.is_null() { 52 | break; 53 | } 54 | // get the request from the task 55 | let req = ngx_http_lua_ffi_get_req(task, nullptr); 56 | // copy the request as response, allocated by C strdup() 57 | // note that both request and response would be freed by nginx 58 | let res = libc::strdup(req); 59 | ngx_http_lua_ffi_respond(task, 0, res, 0); 60 | } 61 | } 62 | println!("exit echo runtime"); 63 | }); 64 | 65 | return 0; 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /examples/rust/echo/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target/ 3 | -------------------------------------------------------------------------------- /examples/rust/echo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "echo" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | bench = false 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | libc = "0.2" 14 | serde_json = "1.0" 15 | -------------------------------------------------------------------------------- /examples/rust/echo/src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use std::ffi::{c_char, c_int, c_void, CStr}; 3 | use std::thread; 4 | 5 | #[derive(Clone)] 6 | struct TaskQueueHandle(*const c_void); 7 | unsafe impl Send for TaskQueueHandle {} 8 | unsafe impl Sync for TaskQueueHandle {} 9 | 10 | extern "C" { 11 | fn ngx_http_lua_ffi_task_poll(p: *const c_void) -> *const c_void; 12 | fn ngx_http_lua_ffi_get_req(tsk: *const c_void, len: *mut c_int) -> *mut c_char; 13 | fn ngx_http_lua_ffi_respond(tsk: *const c_void, rc: c_int, rsp: *const c_char, rsp_len: c_int); 14 | } 15 | 16 | #[no_mangle] 17 | pub extern "C" fn libffi_init(cfg: *mut c_char, tq: *const c_void) -> c_int { 18 | let cfg = unsafe { CStr::from_ptr(cfg).to_string_lossy() }; 19 | let cfg = serde_json::from_str::(&cfg); 20 | if !cfg.is_ok() { 21 | return 1; 22 | } 23 | let cfg: Value = cfg.unwrap(); 24 | 25 | let tq = TaskQueueHandle(tq); 26 | thread::spawn(move || { 27 | println!("cfg: {:?}", cfg); 28 | let tq = tq.clone(); 29 | let nullptr = std::ptr::null_mut(); 30 | loop { 31 | unsafe { 32 | let task = ngx_http_lua_ffi_task_poll(tq.0); 33 | if task.is_null() { 34 | break; 35 | } 36 | let req = ngx_http_lua_ffi_get_req(task, nullptr); 37 | let res = libc::strdup(req); 38 | ngx_http_lua_ffi_respond(task, 0, res, 0); 39 | } 40 | } 41 | println!("exit echo runtime"); 42 | }); 43 | 44 | return 0; 45 | } 46 | -------------------------------------------------------------------------------- /examples/rust/nginx.conf: -------------------------------------------------------------------------------- 1 | daemon off; 2 | error_log /dev/stderr info; 3 | worker_processes auto; 4 | env LD_LIBRARY_PATH; 5 | env RUST_BACKTRACE=full; 6 | 7 | events {} 8 | 9 | http { 10 | server { 11 | listen 20000; 12 | 13 | location /gc { 14 | content_by_lua_block { 15 | collectgarbage() 16 | ngx.say("ok") 17 | } 18 | } 19 | 20 | location /echo { 21 | content_by_lua_block { 22 | local cjson = require("cjson") 23 | local cfg = { name = "hello", value = "world", flag = 200 } 24 | require("resty_ffi") 25 | local demo = ngx.load_ffi("echo", cjson.encode(cfg), {unpin = true}) 26 | local ok, res = demo:echo("foobar") 27 | assert(ok) 28 | assert(res == "foobar") 29 | 30 | demo:__unload() 31 | ok, res = demo:echo("foobar") 32 | assert(not ok) 33 | ngx.log(ngx.ERR, res) 34 | 35 | ngx.say("ok") 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /hot-reload.md: -------------------------------------------------------------------------------- 1 | ## Configuration and Runtime 2 | 3 | For one configuration, lua-resty-ffi will create one dedicated runtime. 4 | 5 | That is, if the configuration is different, the runtime is different. 6 | 7 | Runtimes are independent, even for the same library. 8 | 9 | The runtime could be some threads, or coroutines, or mixed, depending on the language and library. 10 | 11 | Let's recall the lua API: 12 | 13 | `local runtime = ngx.load_ffi(lib, cfg, opts)` 14 | 15 | `cfg`: configuration in any format, e.g. plain string, JSON, or null (sometimes we don't need configuration). 16 | 17 | For Go and Rust, `lib` is the library you compiled for some usage. Different projects compiles out different library file. 18 | 19 | For example, you could write an 20 | [etcd client library](https://github.com/kingluo/lua-resty-ffi/blob/main/examples/go/etcd.go) 21 | and load it with some configuration: 22 | 23 | ```lua 24 | -- here the configuration is the etcd server addresses, in JSON array 25 | local demo = ngx.load_ffi("ffi_go_etcd", "[\"localhost:2379\"]") 26 | ``` 27 | 28 | For Python, Java and Nodejs, the library is the fixed loader library, and the `cfg` is in csv format, 29 | specifing the real module/class to do the init: 30 | 31 | `,,` 32 | 33 | The last part of `cfg` is the real configuration just like you specify in Go and Rust. 34 | 35 | ```lua 36 | -- python 37 | -- libffi_python3.so 38 | -- ffi/kafka.py 39 | local demo = ngx.load_ffi("ffi_python3", 40 | [[ffi.kafka,init,{"servers":"localhost:9092", "topic":"foobar", "group_id": "foobar"}]], 41 | {is_global = true}) 42 | 43 | -- java 44 | -- libffi_java.so 45 | -- demo/http2/App.class 46 | local demo = ngx.load_ffi("ffi_java", "demo.http2.App?,init,") 47 | 48 | -- nodejs 49 | -- libffi_nodejs.so 50 | -- demo/http2.js 51 | local demo = ngx.load_ffi("ffi_nodejs", "demo/http2?,init", {is_global=true}) 52 | ``` 53 | 54 | ## Unload 55 | 56 | When the configuration is changed, we need to release the old configuration, i.e. the old runtime. 57 | 58 | This is done by appending a null message to the request queue of the runtime, which notifies the 59 | poll thread of the runtime to cleanup everything. 60 | The advantage is the shutdown happens after all pending messages get handled, i.e. graceful shutdown. 61 | The disadvantage is it makes some arbitrary time. 62 | 63 | The lua API is: 64 | 65 | `runtime:__unload()` 66 | 67 | ## Code Hot-Reload 68 | 69 | How to apply code changes without reloading/restarting nginx? 70 | 71 | For python, java and nodejs, if you place a question mark suffix in the `module/class`, 72 | the loader library will reload the module/class but not the cached version. 73 | 74 | For example: 75 | 76 | ```lua 77 | local demo = ngx.load_ffi("ffi_python3", "ffi.echo?,init", 78 | {is_global = true, unpin = true}) 79 | ``` 80 | 81 | Each time you call `ngx.load_ffi`, either with different configuration or after unloading the configuration, 82 | the loader will load a fresh `ffi/echo.py` from the filesystem, 83 | then the result runtime will reflect your code change of `ffi/echo.py`. 84 | 85 | ### Python 86 | 87 | Python has builtin module reloading API: [`PyImport_ReloadModule`](https://docs.python.org/3/c-api/import.html#c.PyImport_ReloadModule). 88 | 89 | ### Nodejs 90 | 91 | All loaded modules are cached in [`require.cache`](https://nodejs.org/api/modules.html#requirecache), 92 | so removing the cached version before loading accomplishes the hot-reload. 93 | 94 | ### Java 95 | 96 | Each class is associated with its classloader. 97 | 98 | In brief, writing an **anti-parent-delegation** classloader, 99 | each time you load a class, you create a new classloader to load it, 100 | then hot-reload is done. 101 | 102 | Note that a java file may be compiled into multiple classes, 103 | so hot-realod should happen at the package level of the java file. 104 | 105 | Please check this blog for detail: 106 | 107 | http://luajit.io/post/java-class-reloading/ 108 | 109 | ### Go 110 | 111 | For static and machine-code-compiled languages, it's not easy to do code hot-reload. 112 | 113 | Each `dlopen()` will increase the reference counter of the shared library file handle, 114 | while the `dlclose()` does the reverse. 115 | 116 | When the reference counter becomes `0`, the file handle is removed from the process, 117 | and later `dlopen()` will open a new handle from the filesystem. 118 | 119 | So, the only opportunity to do code hot-reload is clear the reference counter. 120 | However, the reality is more complicated. Some library runtime does not consider cleanup, 121 | which would keep some resources bound to the file handle, e.g. `mmap` somthing from the 122 | shared library, resist some threads in the background, etc. 123 | 124 | Go is the typical example: 125 | 126 | https://github.com/golang/go/issues/11100 127 | 128 | So Go does not support code hot-reload, yet. 129 | 130 | ### Rust 131 | 132 | Just like go, rust has the same issue. 133 | But luckily, rust doesn't need to maintain a language-level runtime. 134 | 135 | So, depending on the implementation of your rust shared library, code hot-reload is possible or not. 136 | 137 | Examples: 138 | 139 | * simple threading 140 | 141 | Yes, it applies the `dlopen()` reference counting, so when all runtimes spawn from the same libraries are 142 | unloaded, you could reload the library in the next loading. 143 | 144 | You could pass `unpin` option to `ngx.load_ffi()` to achieve this goal: 145 | 146 | ```lua 147 | local demo = ngx.load_ffi("echo", cjson.encode(cfg), {unpin = true}) 148 | ``` 149 | 150 | * async rust based on [tokio](https://tokio.rs/) 151 | 152 | No, tokio has complicated mechanism like go. Currently, code hot-reload is not supported. 153 | 154 | ### Notes about `unpin` option 155 | 156 | By default, all shared libraries are disallowd to `dlclose()`, i.e. `unpin` is `false`. 157 | That is, all shared libraries handles are cached forever. 158 | 159 | Reasons: 160 | 161 | * I haven't successfully destroy the Java VM, and it would fail to init the VM the next time. 162 | That is, `(*vm)->DestroyJavaVM(vm)` does not work in my loader code. I will try to solve it in future. 163 | * Go does not support code hot-reload. 164 | * Rust partially supports code hot-reload. 165 | * Python/Java/Nodejs already support code hot-reload in the module/class level. It's rare to reload 166 | the VM itself. 167 | 168 | Interestingly, Python applies perfect `dlopen()/dlclose()` rules, so with `unpin=true`, you could even 169 | reload the python interpreter itself! For example, upgrade the python from 3.8 to 3.11 on-the-fly. 170 | -------------------------------------------------------------------------------- /lua-resty-ffi-callflow.svg: -------------------------------------------------------------------------------- 1 | Note over nginx/envoy worker thread,polling thread: Create runtime 2 | nginx/envoy worker thread -> nginx/envoy worker thread: calls lua_resty_ffi_init() 3 | nginx/envoy worker thread -> polling thread: create 4 | polling thread -> asyncio loop hread: create 5 | Note over nginx/envoy worker thread,polling thread: Invoke a request 6 | nginx/envoy worker thread -> polling thread: post request, yield 7 | polling thread -> asyncio loop hread: send async task 8 | asyncio loop hread --> nginx/envoy worker thread: response, resumenginx/envoy worker threadnginx/envoy worker threadpolling threadpolling threadasyncio loop hreadasyncio loop hreadCreate runtimecalls lua_resty_ffi_init()createcreateInvoke a requestpost request, yieldsend async taskresponse, resume -------------------------------------------------------------------------------- /ngx_http_lua_ffi.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Jinhua Luo (kingluo) luajit.io@gmail.com 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its 16 | * contributors may be used to endorse or promote products derived from 17 | * this software without specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | #ifndef DDEBUG 32 | #define DDEBUG 0 33 | #endif 34 | #include "ddebug.h" 35 | 36 | 37 | #include "ngx_http_lua_common.h" 38 | #include "ngx_http_lua_util.h" 39 | 40 | 41 | #if defined(NGX_THREADS) && defined(NGX_HAVE_EVENTFD) 42 | 43 | 44 | #include 45 | 46 | #ifndef SHARED_OBJECT 47 | 48 | extern void ngx_thread_pool_notify_inject(void *task); 49 | 50 | #else 51 | 52 | #include 53 | 54 | #include 55 | 56 | #include "symbols.h" 57 | #include "build-id.h" 58 | 59 | typedef struct { 60 | ngx_thread_task_t *first; 61 | ngx_thread_task_t **last; 62 | } ngx_thread_pool_queue_t; 63 | 64 | static ngx_atomic_t* ngx_thread_pool_done_lock; 65 | static ngx_thread_pool_queue_t* ngx_thread_pool_done; 66 | static ngx_event_handler_pt ngx_thread_pool_handler; 67 | 68 | int 69 | lua_resty_ffi_init() 70 | { 71 | // check if build-id matches 72 | const struct build_id_note *note = build_id_find_nhdr_by_name(""); 73 | if (!note) 74 | return -1; 75 | 76 | ElfW(Word) len = build_id_length(note); 77 | 78 | const uint8_t *build_id = build_id_data(note); 79 | 80 | char* buf = (char*)calloc(len*2+100, 1); 81 | for (ElfW(Word) i = 0; i < len; i++) { 82 | sprintf(buf + strlen(buf), "%02x", build_id[i]); 83 | } 84 | 85 | if (strcmp(buf, NGX_BUILD_ID) != 0) { 86 | printf("build-id mismatch, expected=%s, actual=%s\n", buf, NGX_BUILD_ID); 87 | free(buf); 88 | return -2; 89 | } 90 | 91 | // resolve symbols 92 | char* addr; 93 | addr = (char*)&ngx_cycle; 94 | addr += NGX_THREAD_POOL_DONE_OFFSET; 95 | ngx_thread_pool_done = (ngx_thread_pool_queue_t*)addr; 96 | 97 | addr = (char*)&ngx_cycle; 98 | addr += NGX_THREAD_POOL_DONE_LOCK_OFFSET; 99 | ngx_thread_pool_done_lock = (ngx_atomic_t*)addr; 100 | 101 | addr = (char*)&ngx_calloc; 102 | addr += NGX_THREAD_POOL_HANDLER_OFFSET; 103 | ngx_thread_pool_handler = (ngx_event_handler_pt)addr; 104 | 105 | free(buf); 106 | 107 | return 0; 108 | } 109 | 110 | static void 111 | ngx_thread_pool_notify_inject(void *data) 112 | { 113 | ngx_thread_task_t *task = data; 114 | 115 | task->next = NULL; 116 | 117 | ngx_spinlock(ngx_thread_pool_done_lock, 1, 2048); 118 | 119 | *((*ngx_thread_pool_done).last) = task; 120 | (*ngx_thread_pool_done).last = &task->next; 121 | 122 | ngx_memory_barrier(); 123 | 124 | ngx_unlock(ngx_thread_pool_done_lock); 125 | 126 | (void) ngx_notify(ngx_thread_pool_handler); 127 | } 128 | 129 | #endif 130 | 131 | 132 | typedef struct { 133 | ngx_http_lua_co_ctx_t *wait_co_ctx; 134 | int rc; 135 | char *req; 136 | int req_len; 137 | char *rsp; 138 | int rsp_len; 139 | int is_abort:1; 140 | } ffi_task_ctx_t; 141 | 142 | 143 | typedef struct { 144 | ngx_thread_task_t *first; 145 | ngx_thread_task_t **last; 146 | } ngx_task_queue_t; 147 | 148 | #define ngx_task_queue_init(q) \ 149 | (q)->first = NULL; \ 150 | (q)->last = &(q)->first 151 | 152 | 153 | typedef struct { 154 | ngx_thread_mutex_t mtx; 155 | ngx_task_queue_t queue; 156 | ngx_int_t waiting; 157 | ngx_thread_cond_t cond; 158 | ngx_log_t *log; 159 | ngx_int_t max_queue; 160 | } ffi_task_queue_t; 161 | 162 | 163 | char* 164 | ngx_http_lua_ffi_get_req(void *tsk, int *len) 165 | { 166 | ngx_thread_task_t *task = tsk; 167 | ffi_task_ctx_t *ctx = task->ctx; 168 | if (len != NULL) { 169 | *len = ctx->req_len; 170 | } 171 | return ctx->req; 172 | } 173 | 174 | 175 | void 176 | ngx_http_lua_ffi_respond(void *tsk, int rc, char* rsp, int rsp_len) 177 | { 178 | ngx_thread_task_t *task = tsk; 179 | ffi_task_ctx_t *ctx = task->ctx; 180 | ctx->rc = rc; 181 | ctx->rsp = rsp; 182 | ctx->rsp_len = rsp_len; 183 | ngx_thread_pool_notify_inject(task); 184 | } 185 | 186 | 187 | /* 188 | * Re-implement ngx_thread_task_alloc to avoid alloc from request pool 189 | * since the request may exit before worker thread finish. 190 | * And we may implement a memory pool for this allocation in the future 191 | * to avoid memory fragmentation. 192 | */ 193 | static ngx_thread_task_t * 194 | ngx_http_lua_ffi_task_alloc(size_t size) 195 | { 196 | ngx_thread_task_t *task; 197 | 198 | task = ngx_calloc(sizeof(ngx_thread_task_t) + size, ngx_cycle->log); 199 | if (task == NULL) { 200 | return NULL; 201 | } 202 | 203 | task->ctx = task + 1; 204 | 205 | return task; 206 | } 207 | 208 | 209 | static void 210 | ngx_http_lua_ffi_task_free(ffi_task_ctx_t *ctx) 211 | { 212 | if (ctx->req) { 213 | free(ctx->req); 214 | } 215 | if (ctx->rsp) { 216 | free(ctx->rsp); 217 | } 218 | ngx_thread_task_t *task = (void*)ctx; 219 | ngx_free(task - 1); 220 | } 221 | 222 | 223 | static ngx_int_t 224 | ngx_http_lua_ffi_resume(ngx_http_request_t *r) 225 | { 226 | lua_State *vm; 227 | ngx_connection_t *c; 228 | ngx_int_t rc; 229 | ngx_uint_t nreqs; 230 | ngx_http_lua_ctx_t *ctx; 231 | 232 | ctx = ngx_http_get_module_ctx(r, ngx_http_lua_module); 233 | if (ctx == NULL) { 234 | return NGX_ERROR; 235 | } 236 | 237 | ctx->resume_handler = ngx_http_lua_wev_handler; 238 | 239 | c = r->connection; 240 | vm = ngx_http_lua_get_lua_vm(r, ctx); 241 | nreqs = c->requests; 242 | 243 | rc = ngx_http_lua_run_thread(vm, r, ctx, 3); 244 | 245 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 246 | "lua run thread returned %d", rc); 247 | 248 | if (rc == NGX_AGAIN) { 249 | return ngx_http_lua_run_posted_threads(c, vm, r, ctx, nreqs); 250 | } 251 | 252 | if (rc == NGX_DONE) { 253 | ngx_http_lua_finalize_request(r, NGX_DONE); 254 | return ngx_http_lua_run_posted_threads(c, vm, r, ctx, nreqs); 255 | } 256 | 257 | /* rc == NGX_ERROR || rc >= NGX_OK */ 258 | 259 | if (ctx->entered_content_phase) { 260 | ngx_http_lua_finalize_request(r, rc); 261 | return NGX_DONE; 262 | } 263 | 264 | return rc; 265 | } 266 | 267 | 268 | /* executed in nginx event loop */ 269 | static void 270 | ngx_http_lua_ffi_event_handler(ngx_event_t *ev) 271 | { 272 | ffi_task_ctx_t *ffi_ctx; 273 | lua_State *L; 274 | ngx_http_request_t *r; 275 | ngx_connection_t *c; 276 | ngx_http_lua_ctx_t *ctx; 277 | 278 | ffi_ctx = ev->data; 279 | 280 | if (ffi_ctx->is_abort) { 281 | goto failed; 282 | } 283 | 284 | L = ffi_ctx->wait_co_ctx->co; 285 | 286 | r = ngx_http_lua_get_req(L); 287 | if (r == NULL) { 288 | goto failed; 289 | } 290 | 291 | c = r->connection; 292 | 293 | ctx = ngx_http_get_module_ctx(r, ngx_http_lua_module); 294 | if (ctx == NULL) { 295 | goto failed; 296 | } 297 | 298 | lua_pushboolean(L, ffi_ctx->rc ? 0 : 1); 299 | 300 | if (ffi_ctx->rc) { 301 | lua_pushinteger(L, ffi_ctx->rc); 302 | } 303 | 304 | if (ffi_ctx->rsp) { 305 | if (ffi_ctx->rsp_len) { 306 | lua_pushlstring(L, ffi_ctx->rsp, ffi_ctx->rsp_len); 307 | } else { 308 | lua_pushstring(L, ffi_ctx->rsp); 309 | } 310 | } else { 311 | lua_pushnil(L); 312 | } 313 | 314 | if (!ffi_ctx->rc) { 315 | lua_pushnil(L); 316 | } 317 | 318 | ctx->cur_co_ctx = ffi_ctx->wait_co_ctx; 319 | ctx->cur_co_ctx->cleanup = NULL; 320 | 321 | ngx_http_lua_ffi_task_free(ffi_ctx); 322 | 323 | /* resume the caller coroutine */ 324 | 325 | if (ctx->entered_content_phase) { 326 | (void) ngx_http_lua_ffi_resume(r); 327 | 328 | } else { 329 | ctx->resume_handler = ngx_http_lua_ffi_resume; 330 | ngx_http_core_run_phases(r); 331 | } 332 | 333 | ngx_http_run_posted_requests(c); 334 | 335 | return; 336 | 337 | failed: 338 | 339 | ngx_http_lua_ffi_task_free(ffi_ctx); 340 | return; 341 | } 342 | 343 | 344 | static void 345 | ngx_http_lua_ffi_cleanup(void *data) 346 | { 347 | ngx_http_lua_co_ctx_t *ctx = data; 348 | ffi_task_ctx_t *ffi_ctx = ctx->data; 349 | ffi_ctx->is_abort = 1; 350 | } 351 | 352 | 353 | void* 354 | ngx_http_lua_ffi_create_task_queue(int max_queue) 355 | { 356 | ffi_task_queue_t* tp = ngx_calloc(sizeof(ffi_task_queue_t), ngx_cycle->log); 357 | 358 | ngx_task_queue_init(&tp->queue); 359 | 360 | if (ngx_thread_mutex_create(&tp->mtx, ngx_cycle->log) != NGX_OK) { 361 | ngx_free(tp); 362 | return NULL; 363 | } 364 | 365 | if (ngx_thread_cond_create(&tp->cond, ngx_cycle->log) != NGX_OK) { 366 | (void) ngx_thread_mutex_destroy(&tp->mtx, ngx_cycle->log); 367 | ngx_free(tp); 368 | return NULL; 369 | } 370 | 371 | tp->log = ngx_cycle->log; 372 | tp->max_queue = max_queue; 373 | 374 | return tp; 375 | } 376 | 377 | 378 | static void 379 | ngx_http_lua_ffi_free_task_queue(ffi_task_queue_t* tp) 380 | { 381 | /* 382 | * After special finished task, no new task post (ensured by lua land), 383 | * and all pending tasks are handled, so no tasks in the queue now. 384 | */ 385 | ngx_thread_mutex_destroy(&tp->mtx, ngx_cycle->log); 386 | ngx_thread_cond_destroy(&tp->cond, ngx_cycle->log); 387 | ngx_free(tp); 388 | } 389 | 390 | 391 | static ngx_thread_task_t* 392 | ngx_http_lua_ffi_create_task(ngx_http_request_t *r) 393 | { 394 | ngx_http_lua_ctx_t *ctx; 395 | ngx_thread_task_t *task; 396 | ffi_task_ctx_t *ffi_ctx; 397 | 398 | ctx = ngx_http_get_module_ctx(r, ngx_http_lua_module); 399 | if (ctx == NULL) { 400 | return NULL; 401 | } 402 | 403 | task = ngx_http_lua_ffi_task_alloc(sizeof(ffi_task_ctx_t)); 404 | 405 | if (task == NULL) { 406 | return NULL; 407 | } 408 | 409 | ffi_ctx = task->ctx; 410 | 411 | ffi_ctx->wait_co_ctx = ctx->cur_co_ctx; 412 | 413 | ctx->cur_co_ctx->cleanup = ngx_http_lua_ffi_cleanup; 414 | ctx->cur_co_ctx->data = ffi_ctx; 415 | 416 | task->event.handler = ngx_http_lua_ffi_event_handler; 417 | task->event.data = ffi_ctx; 418 | 419 | return task; 420 | } 421 | 422 | 423 | static ngx_int_t 424 | ngx_task_post(ffi_task_queue_t *tp, ngx_thread_task_t *task) 425 | { 426 | if (task->event.active) { 427 | ngx_log_error(NGX_LOG_ALERT, tp->log, 0, 428 | "task #%ui already active", task->id); 429 | return NGX_ERROR; 430 | } 431 | 432 | if (ngx_thread_mutex_lock(&tp->mtx, tp->log) != NGX_OK) { 433 | return NGX_ERROR; 434 | } 435 | 436 | if (task->ctx && tp->waiting >= tp->max_queue) { 437 | (void) ngx_thread_mutex_unlock(&tp->mtx, tp->log); 438 | 439 | ngx_log_error(NGX_LOG_ERR, tp->log, 0, 440 | "queue %p overflow: %i tasks waiting", 441 | tp, tp->waiting); 442 | return NGX_ERROR; 443 | } 444 | 445 | task->event.active = 1; 446 | 447 | task->id = 0; 448 | task->next = NULL; 449 | 450 | if (ngx_thread_cond_signal(&tp->cond, tp->log) != NGX_OK) { 451 | (void) ngx_thread_mutex_unlock(&tp->mtx, tp->log); 452 | return NGX_ERROR; 453 | } 454 | 455 | *tp->queue.last = task; 456 | tp->queue.last = &task->next; 457 | 458 | tp->waiting++; 459 | 460 | (void) ngx_thread_mutex_unlock(&tp->mtx, tp->log); 461 | 462 | return NGX_OK; 463 | } 464 | 465 | 466 | int 467 | ngx_http_lua_ffi_task_post(void *p0, void* p, char* req, int req_len) 468 | { 469 | ngx_http_request_t *r = p0; 470 | ffi_task_queue_t *tp = p; 471 | ngx_thread_task_t *task = ngx_http_lua_ffi_create_task(r); 472 | ffi_task_ctx_t *ctx = task->ctx; 473 | ctx->req = req; 474 | ctx->req_len = req_len; 475 | if (ngx_task_post(tp, task) != NGX_OK) { 476 | ngx_http_lua_ffi_task_free(ctx); 477 | return NGX_ERROR; 478 | } 479 | return NGX_OK; 480 | } 481 | 482 | 483 | int 484 | ngx_http_lua_ffi_task_finish(void* p) 485 | { 486 | ffi_task_queue_t *tp = p; 487 | 488 | ngx_thread_task_t *task = ngx_calloc(sizeof(ngx_thread_task_t), ngx_cycle->log); 489 | 490 | /* append, so that all pending tasks get handled before finished */ 491 | if (ngx_task_post(tp, task) != NGX_OK) { 492 | ngx_free(task); 493 | return NGX_ERROR; 494 | } 495 | 496 | return NGX_OK; 497 | } 498 | 499 | 500 | void* 501 | ngx_http_lua_ffi_task_poll(void *p) 502 | { 503 | ffi_task_queue_t *tp = p; 504 | ngx_thread_task_t *task = NULL; 505 | if (ngx_thread_mutex_lock(&tp->mtx, tp->log) != NGX_OK) { 506 | return NULL; 507 | } 508 | 509 | /* the number may become negative */ 510 | tp->waiting--; 511 | 512 | while (tp->queue.first == NULL) { 513 | if (ngx_thread_cond_wait(&tp->cond, &tp->mtx, tp->log) 514 | != NGX_OK) 515 | { 516 | (void) ngx_thread_mutex_unlock(&tp->mtx, tp->log); 517 | return NULL; 518 | } 519 | } 520 | 521 | task = tp->queue.first; 522 | tp->queue.first = task->next; 523 | 524 | if (tp->queue.first == NULL) { 525 | tp->queue.last = &tp->queue.first; 526 | } 527 | 528 | if (ngx_thread_mutex_unlock(&tp->mtx, tp->log) != NGX_OK) { 529 | return NULL; 530 | } 531 | 532 | if (task->ctx) { 533 | return task; 534 | } 535 | 536 | ngx_free(task); 537 | ngx_http_lua_ffi_free_task_queue(tp); 538 | return NULL; 539 | } 540 | 541 | 542 | #endif 543 | 544 | /* vi:set ft=c ts=4 sw=4 et fdm=marker: */ 545 | -------------------------------------------------------------------------------- /resty_ffi.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Copyright (c) 2022, Jinhua Luo (kingluo) luajit.io@gmail.com 3 | -- All rights reserved. 4 | -- 5 | -- Redistribution and use in source and binary forms, with or without 6 | -- modification, are permitted provided that the following conditions are met: 7 | -- 8 | -- 1. Redistributions of source code must retain the above copyright notice, this 9 | -- list of conditions and the following disclaimer. 10 | -- 11 | -- 2. Redistributions in binary form must reproduce the above copyright notice, 12 | -- this list of conditions and the following disclaimer in the documentation 13 | -- and/or other materials provided with the distribution. 14 | -- 15 | -- 3. Neither the name of the copyright holder nor the names of its 16 | -- contributors may be used to endorse or promote products derived from 17 | -- this software without specific prior written permission. 18 | -- 19 | -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | -- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | -- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | -- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | -- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | -- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | -- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | -- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | -- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | -- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -- 30 | local ffi = require "ffi" 31 | local C = ffi.C 32 | local get_request 33 | local ok, base = pcall(require, "resty.core.base") 34 | if ok then 35 | get_request = base.get_request 36 | end 37 | 38 | local init = false 39 | local function test_symbol(sym) return C[sym] ~= nil end 40 | local resty_ffi = C 41 | 42 | local runtimes = {} 43 | 44 | ffi.cdef[[ 45 | void *malloc(size_t size); 46 | void free(void *ptr); 47 | int libffi_init(char* cfg, void *tq); 48 | void* ngx_http_lua_ffi_create_task_queue(int max_queue); 49 | int ngx_http_lua_ffi_task_post(void *r, void* tq, char* req, int req_len); 50 | int ngx_http_lua_ffi_task_finish(void *p); 51 | int lua_resty_ffi_init(); 52 | ]] 53 | 54 | local function post(self, req, wrapper) 55 | if self.finished then 56 | return false, "task queue is finished" 57 | end 58 | local buf = nil 59 | local buf_len = 0 60 | if req then 61 | buf_len = #req 62 | buf = C.malloc(#req + 1) 63 | ffi.copy(buf, req) 64 | end 65 | local r = wrapper and wrapper:this() or get_request() 66 | local ret = resty_ffi.ngx_http_lua_ffi_task_post(r, self.tq, buf, buf_len) 67 | if ret ~= 0 then 68 | return false, "post failed, queue full" 69 | end 70 | if wrapper then 71 | return wrapper:ffiYield() 72 | end 73 | return coroutine._yield() 74 | end 75 | 76 | local function unload(self) 77 | if not self.finished then 78 | self.finished = true 79 | self.handle = nil 80 | resty_ffi.ngx_http_lua_ffi_task_finish(self.tq) 81 | self.tq = nil 82 | runtimes[self.key] = nil 83 | self.key = nil 84 | end 85 | end 86 | 87 | local mt = { 88 | __index = function(self, k) 89 | rawset(self, k, post) 90 | return post 91 | end, 92 | __call = post, 93 | } 94 | 95 | local pin_libs = {} 96 | 97 | local function ffi_load(lib, is_global, is_pin) 98 | local handle = ffi.load(lib, is_global) 99 | if handle and is_pin then 100 | pin_libs[lib] = handle 101 | end 102 | return handle 103 | end 104 | 105 | ngx = ngx or {} 106 | ngx.load_ffi = function(lib, cfg, opts) 107 | if not init then 108 | if not pcall(test_symbol, "ngx_http_lua_ffi_create_task_queue") then 109 | local handle = ffi.load("resty_ffi", true) 110 | if handle.lua_resty_ffi_init() ~= 0 then 111 | error("lua_resty_ffi_init() failed: nginx build-id not found or mismatch") 112 | end 113 | resty_ffi = handle 114 | end 115 | init = true 116 | end 117 | 118 | local max_queue = 65536 119 | local is_global = false 120 | if opts ~= nil then 121 | if opts.max_queue ~= nil then 122 | max_queue = opts.max_queue 123 | end 124 | if opts.is_global ~= nil then 125 | is_global = opts.is_global 126 | end 127 | end 128 | 129 | local key = lib 130 | if cfg then 131 | key = lib .. "&" .. cfg 132 | end 133 | if runtimes[key] then 134 | return runtimes[key] 135 | end 136 | 137 | local is_pin = (not opts) or (not opts.unpin) 138 | local tq = resty_ffi.ngx_http_lua_ffi_create_task_queue(max_queue) 139 | local nffi = setmetatable({ 140 | finished = false, 141 | key = key, 142 | handle = ffi_load(lib, is_global, is_pin), 143 | tq = tq, 144 | __unload = unload, 145 | }, mt) 146 | 147 | local buf = nil 148 | if cfg then 149 | buf = C.malloc(#cfg + 1) 150 | ffi.copy(buf, cfg) 151 | end 152 | local rc = nffi.handle.libffi_init(buf, tq) 153 | if buf then 154 | C.free(buf) 155 | end 156 | if rc == 0 then 157 | runtimes[key] = nffi 158 | return nffi 159 | end 160 | return nil, rc 161 | end 162 | -------------------------------------------------------------------------------- /rockspec/lua-resty-ffi-main-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-ffi" 2 | version = "main-1" 3 | source = { 4 | url = "git+https://github.com/kingluo/lua-resty-ffi", 5 | branch = "main", 6 | } 7 | 8 | description = { 9 | summary = "efficient and generic API to do hybrid programming in openresty", 10 | detailed = [[ 11 | With lua-resty-ffi, You could develop openresty libraries in mainstream languages (Go, Python, Java, Rust, Node.js). 12 | 13 | Features: 14 | 15 | * nonblocking, in coroutine way 16 | 17 | * simple but extensible interface, supports any C ABI compliant language 18 | 19 | * once and for all, no need to write C/Lua codes to do coupling anymore 20 | 21 | * high performance, faster than unix domain socket way 22 | 23 | * generic loader library for python/java/nodejs 24 | 25 | * any serialization message format you like 26 | ]], 27 | homepage = "https://github.com/kingluo/lua-resty-ffi", 28 | license = " BSD-3-Clause", 29 | maintainer = "Jinhua Luo ", 30 | } 31 | 32 | dependencies = { 33 | } 34 | 35 | build = { 36 | type = "make", 37 | makefile = "Makefile.rocks", 38 | build_variables = { 39 | CFLAGS="$(CFLAGS)", 40 | LIBFLAG="$(LIBFLAG)", 41 | LUA_LIBDIR="$(LUA_LIBDIR)", 42 | LUA_BINDIR="$(LUA_BINDIR)", 43 | LUA_INCDIR="$(LUA_INCDIR)", 44 | LUA="$(LUA)", 45 | OR_SRC="$(OR_SRC)", 46 | NGINX_BIN="$(NGINX_BIN)", 47 | }, 48 | install_variables = { 49 | INST_PREFIX="$(PREFIX)", 50 | INST_BINDIR="$(BINDIR)", 51 | INST_LIBDIR="$(LIBDIR)", 52 | INST_LUADIR="$(LUADIR)", 53 | INST_CONFDIR="$(CONFDIR)", 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /rockspec/lua-resty-ffi-main-2.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-ffi" 2 | version = "main-2" 3 | source = { 4 | url = "git+https://github.com/kingluo/lua-resty-ffi", 5 | branch = "main", 6 | } 7 | 8 | description = { 9 | summary = "efficient and generic API to do hybrid programming in openresty", 10 | detailed = [[ 11 | With lua-resty-ffi, You could develop openresty libraries in mainstream languages (Go, Python, Java, Rust, Node.js). 12 | 13 | Features: 14 | 15 | * nonblocking, in coroutine way 16 | 17 | * simple but extensible interface, supports any C ABI compliant language 18 | 19 | * once and for all, no need to write C/Lua codes to do coupling anymore 20 | 21 | * high performance, faster than unix domain socket way 22 | 23 | * generic loader library for python/java/nodejs 24 | 25 | * any serialization message format you like 26 | ]], 27 | homepage = "https://github.com/kingluo/lua-resty-ffi", 28 | license = " BSD-3-Clause", 29 | maintainer = "Jinhua Luo ", 30 | } 31 | 32 | dependencies = { 33 | } 34 | 35 | build = { 36 | type = "make", 37 | makefile = "Makefile.rocks", 38 | build_variables = { 39 | CFLAGS="$(CFLAGS)", 40 | LIBFLAG="$(LIBFLAG)", 41 | LUA_LIBDIR="$(LUA_LIBDIR)", 42 | LUA_BINDIR="$(LUA_BINDIR)", 43 | LUA_INCDIR="$(LUA_INCDIR)", 44 | LUA="$(LUA)", 45 | OR_SRC="$(OR_SRC)", 46 | NGINX_BIN="$(NGINX_BIN)", 47 | }, 48 | install_variables = { 49 | INST_PREFIX="$(PREFIX)", 50 | INST_BINDIR="$(BINDIR)", 51 | INST_LIBDIR="$(LIBDIR)", 52 | INST_LUADIR="$(LUADIR)", 53 | INST_CONFDIR="$(CONFDIR)", 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /rockspec/lua-resty-ffi-python-main-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-ffi-python" 2 | version = "main-1" 3 | source = { 4 | url = "git+https://github.com/kingluo/lua-resty-ffi", 5 | branch = "main", 6 | } 7 | 8 | description = { 9 | summary = "lua-resty-ffi python loader library", 10 | homepage = "https://github.com/kingluo/lua-resty-ffi", 11 | license = " BSD-3-Clause", 12 | maintainer = "Jinhua Luo ", 13 | } 14 | 15 | dependencies = { 16 | "lua-resty-ffi = main-1", 17 | } 18 | 19 | build = { 20 | type = "make", 21 | makefile = "Makefile-python.rocks", 22 | build_variables = { 23 | CFLAGS="$(CFLAGS)", 24 | LIBFLAG="$(LIBFLAG)", 25 | LUA_LIBDIR="$(LUA_LIBDIR)", 26 | }, 27 | install_variables = { 28 | INST_PREFIX="$(PREFIX)", 29 | INST_LIBDIR="$(LIBDIR)", 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /rockspec/lua-resty-ffi-python-main-2.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-ffi-python" 2 | version = "main-2" 3 | source = { 4 | url = "git+https://github.com/kingluo/lua-resty-ffi", 5 | branch = "main", 6 | } 7 | 8 | description = { 9 | summary = "lua-resty-ffi python loader library", 10 | homepage = "https://github.com/kingluo/lua-resty-ffi", 11 | license = " BSD-3-Clause", 12 | maintainer = "Jinhua Luo ", 13 | } 14 | 15 | dependencies = { 16 | "lua-resty-ffi = main-2", 17 | } 18 | 19 | build = { 20 | type = "make", 21 | makefile = "Makefile-python.rocks", 22 | build_variables = { 23 | CFLAGS="$(CFLAGS)", 24 | LIBFLAG="$(LIBFLAG)", 25 | LUA_LIBDIR="$(LUA_LIBDIR)", 26 | }, 27 | install_variables = { 28 | INST_PREFIX="$(PREFIX)", 29 | INST_LIBDIR="$(LIBDIR)", 30 | }, 31 | } 32 | --------------------------------------------------------------------------------