From 349bc47ef2a88d93f86531f7d7fd898c95daad27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Vavru=C5=A1a?= <marek.vavrusa@nic.cz> Date: Mon, 19 Oct 2015 19:41:03 +0200 Subject: [PATCH] modules: support for modules in Go (needs golang 1.5+) --- Makefile | 5 ++- doc/build.rst | 3 +- help.mk | 2 +- lib/module.c | 53 ------------------------------ modules/README.rst | 66 +++++++++++++++++--------------------- modules/gostats/gostats.go | 30 ++++++++++------- modules/modules.mk | 15 +++------ platform.mk | 14 ++++++-- tests/mock_gomodule.c | 44 ------------------------- tests/test_module.c | 8 ----- tests/unit.mk | 2 -- 11 files changed, 71 insertions(+), 171 deletions(-) delete mode 100644 tests/mock_gomodule.c diff --git a/Makefile b/Makefile index ea55233d7..0797fee85 100644 --- a/Makefile +++ b/Makefile @@ -21,12 +21,15 @@ $(eval $(call find_alt,lua,luajit)) $(eval $(call find_lib,cmocka)) $(eval $(call find_bin,doxygen)) $(eval $(call find_bin,sphinx-build)) -$(eval $(call find_bin,gccgo)) $(eval $(call find_bin,python)) $(eval $(call find_lib,libmemcached,1.0)) $(eval $(call find_lib,hiredis)) $(eval $(call find_lib,socket_wrapper)) $(eval $(call find_lib,libdnssec)) +# Find Go compiler version +E := +GO_VERSION := $(subst $(E) $(E),,$(subst go,,$(wordlist 1,3,$(subst ., ,$(word 3,$(shell $(GO) version)))))) +$(eval $(call find_ver,go,$(GO_VERSION),150)) # Work around luajit on OS X ifeq ($(PLATFORM), Darwin) diff --git a/doc/build.rst b/doc/build.rst index cf833cbd5..a2b02c316 100644 --- a/doc/build.rst +++ b/doc/build.rst @@ -41,6 +41,7 @@ There are also *optional* packages that enable specific functionality in Knot DN "libmemcached_", "``modules/memcached``", "To build memcached backend module." "hiredis_", "``modules/redis``", "To build redis backend module." + "Go_ 1.5+", "``modules``", "Build modules written in Go." "cmocka_", "``unit tests``", "Unit testing framework." "Python_", "``integration tests``", "For test scripts." "Doxygen_", "``documentation``", "Generating API documentation." @@ -134,7 +135,7 @@ The project can be built with code coverage tracking using the ``COVERAGE=1`` va .. _Lua: http://www.lua.org/about.html .. _LuaJIT: http://luajit.org/luajit.html -.. _GCCGO: https://golang.org/doc/install/gccgo +.. _Go: https://golang.org .. _libmemcached: http://libmemcached.org/libMemcached.html .. _hiredis: https://github.com/redis/hiredis .. _Doxygen: http://www.stack.nl/~dimitri/doxygen/manual/index.html diff --git a/help.mk b/help.mk index 416903741..096faeaae 100644 --- a/help.mk +++ b/help.mk @@ -18,7 +18,7 @@ info: $(info Optional) $(info --------) $(info [$(HAS_doxygen)] doxygen (doc)) - $(info [$(HAS_gccgo)] GCCGO (modules/go)) + $(info [$(HAS_go)] Go (modules/go)) $(info [$(HAS_libmemcached)] libmemcached (modules/memcached)) $(info [$(HAS_hiredis)] hiredis (modules/redis)) $(info [$(HAS_cmocka)] cmocka (tests/unit)) diff --git a/lib/module.c b/lib/module.c index 071394940..6b4316247 100644 --- a/lib/module.c +++ b/lib/module.c @@ -121,54 +121,6 @@ static int load_sym_c(struct kr_module *module, uint32_t api_required) return kr_ok(); } -/** Bootstrap Go runtime from module. */ -static int bootstrap_libgo(struct kr_module *module) -{ - /* Check if linked against compatible libgo */ - void (*go_check)(void) = dlsym(module->lib, "runtime_check"); - void (*go_args)(int, void*) = dlsym(module->lib, "runtime_args"); - void (*go_init_os)(void) = dlsym(module->lib, "runtime_osinit"); - void (*go_init_sched)(void) = dlsym(module->lib, "runtime_schedinit"); - void (*go_init_main)(void) = dlsym(module->lib, "__go_init_main"); - if ((go_check && go_args && go_init_os && go_init_sched && go_init_main) == false) { - return kr_error(EINVAL); - } - - /* - * Bootstrap runtime - this is minimal runtime, we would need a running scheduler - * and gc for coroutines and memory allocation. That would require a custom "world loop", - * message passing, and either runtime sharing or module isolation. - * https://github.com/gcc-mirror/gcc/blob/gcc-4_9_2-release/libgo/runtime/proc.c#L457 - */ - char *fake_argv[2] = { - getenv("_"), - NULL - }; - go_check(); - go_args(1, fake_argv); - go_init_os(); - go_init_sched(); - go_init_main(); - - return kr_ok(); -} - -/** Load Go module symbols. */ -static int load_ffi_go(struct kr_module *module, uint32_t api_required) -{ - /* Bootstrap libgo */ - int ret = bootstrap_libgo(module); - if (ret != 0) { - return ret; - } - - /* Enforced prefix for now. */ - const char *module_prefix = "main."; - ABI_CHECK(module, module_prefix, "Api", api_required); - ABI_LOAD(module, module_prefix, "Init", "Deinit", "Config", "Layer", "Props"); - return kr_ok(); -} - int kr_module_load(struct kr_module *module, const char *name, const char *path) { if (module == NULL || name == NULL) { @@ -197,11 +149,6 @@ int kr_module_load(struct kr_module *module, const char *name, const char *path) /* Try to load module ABI. */ int ret = load_sym_c(module, KR_MODULE_API); - if (ret != 0 && module->lib != RTLD_DEFAULT) { - ret = load_ffi_go(module, KR_MODULE_API); - } - - /* Module constructor. */ if (ret == 0 && module->init) { ret = module->init(module); } diff --git a/modules/README.rst b/modules/README.rst index 47b1435b9..8cfb97341 100644 --- a/modules/README.rst +++ b/modules/README.rst @@ -11,11 +11,8 @@ Modules API reference Supported languages =================== -Currently modules written in C and Lua are supported. -There is also a rudimentary support for writing modules in Go |---| -(1) the library has no native Go bindings, library is accessible using CGO_, -(2) gc doesn't support building shared libraries, GCCGO_ is required, -(3) no coroutines and no garbage collecting thread, as the Go code is called from C threads. +Currently modules written in C and LuaJIT are supported. +There is also a support for writing modules in Go 1.5+ |---| the library has no native Go bindings, library is accessible using CGO_. The anatomy of an extension =========================== @@ -25,14 +22,14 @@ A module is a shared object or script defining specific functions, here's an ove *Note* |---| the :ref:`Modules <lib_api_modules>` header documents the module loading and API. .. csv-table:: - :header: "C", "Lua", "Go", "Params", "Comment" + :header: "C/Go", "Lua", "Params", "Comment" - "``X_api()`` [#]_", "", "``Api()``", "", "API version" - "``X_init()``", "``X.init()``", "``Init()``", "``module``", "Constructor" - "``X_deinit()``", "``X.deinit()``", "``Deinit()``", "``module, key``", "Destructor" - "``X_config()``", "``X.config()``", "``Config()``", "``module``", "Configuration" - "``X_layer()``", "``X.layer``", "``Layer()``", "``module``", ":ref:`Module layer <lib-layers>`" - "``X_props()``", "", "``Props()``", "", "List of properties" + "``X_api()`` [#]_", "", "", "API version" + "``X_init()``", "``X.init()``", "``module``", "Constructor" + "``X_deinit()``", "``X.deinit()``", "``module, key``", "Destructor" + "``X_config()``", "``X.config()``", "``module``", "Configuration" + "``X_layer()``", "``X.layer``", "``module``", ":ref:`Module layer <lib-layers>`" + "``X_props()``", "", "", "List of properties" .. [#] Mandatory symbol. @@ -181,9 +178,7 @@ and publish data about query resolution. Writing a module in Go ====================== -.. note:: At the moment only a limited subset of Go is supported. The reason is that the Go functions must run inside the goroutines, and *presume* the garbage collector and scheduler are running in the background. `GCCGO`_ compiler can build dynamic libraries, and also allow us to bootstrap basic Go runtime, including a trampoline to call Go functions. The problem with the ``layer()`` and callbacks is that they're called from C threads, that Go runtime has no knowledge of. Thus neither garbage collection or spawning routines can work. The solution could be to register C threads to Go runtime, or have each module to run inside its world loop and use IPC instead of callbacks |---| alas neither is implemented at the moment, but may be in the future. - -The Go modules also use CGO_ to interface C resolver library, and to declare layers with function pointers, which are `not present in Go`_. Each module must be the ``main`` package, here's a minimal example: +The Go modules use CGO_ to interface C resolver library, there are no native bindings yet. Second issue is that layers are declared as a structure of function pointers, which are `not present in Go`_, the workaround is to declare them in CGO_ header. Each module must be the ``main`` package, here's a minimal example: .. code-block:: go @@ -195,10 +190,16 @@ The Go modules also use CGO_ to interface C resolver library, and to declare lay import "C" import "unsafe" - func Api() C.uint32_t { + //export mymodule_api + func mymodule_api() C.uint32_t { return C.KR_MODULE_API } + // Mandatory function + func main() {} + +.. warning:: Do not forget to prefix function declarations with ``//export symbol_name``, as only these will be exported in module. + In order to integrate with query processing, you have to declare a helper function with function pointers to the the layer implementation. Since the code prefacing ``import "C"`` is expanded in headers, you need the `static inline` trick to avoid multiple declarations. Here's how the preface looks like: @@ -206,45 +207,37 @@ to avoid multiple declarations. Here's how the preface looks like: .. code-block:: go /* + #include "lib/layer.h" #include "lib/module.h" - #include "lib/layer.h" - - //! Trampoline for Go callbacks, note that this is going to work - //! with ELF only, this is hopefully going to change in the future - extern int Begin(knot_layer_t *, void *) __asm__ ("main.Begin"); - extern int Finish(knot_layer_t *) __asm__ ("main.Finish"); - static inline const knot_layer_api_t *_gostats_layer(void) + // Need a forward declaration of the function signature + int finish(knot_layer_t *); + // Workaround for layers composition + static inline const knot_layer_api_t *_layer(void) { static const knot_layer_api_t api = { - .begin = &Begin, - .finish = &Finish + .finish = &finish }; return &api; } */ import "C" import "unsafe" - import "fmt" -Now we can add the implementations for the ``Begin`` and ``Finish`` functions, and finalize the module: +Now we can add the implementations for the ``finish`` layer and finalize the module: .. code-block:: go - func Begin(ctx *C.knot_layer_t, param unsafe.Pointer) C.int { - // Save the context - ctx.data = param - return 0 - } - - func Finish(ctx *C.knot_layer_t) C.int { + //export finish + func finish(ctx *C.knot_layer_t) C.int { // Since the context is unsafe.Pointer, we need to cast it var param *C.struct_kr_request = (*C.struct_kr_request)(ctx.data) // Now we can use the C API as well - fmt.Printf("[go] resolved %d queries", C.list_size(¶m.rplan.resolved)) + fmt.Printf("[go] resolved %d queries\n", C.list_size(¶m.rplan.resolved)) return 0 } - func Layer(module *C.struct_kr_module) *C.knot_layer_api_t { + //export mymodule_layer + func mymodule_layer(module *C.struct_kr_module) *C.knot_layer_api_t { // Wrapping the inline trampoline function return C._layer() } @@ -330,6 +323,5 @@ regular tables. .. _`not present in Go`: http://blog.golang.org/gos-declaration-syntax .. _CGO: http://golang.org/cmd/cgo/ -.. _GCCGO: https://golang.org/doc/install/gccgo .. |---| unicode:: U+02014 .. em dash \ No newline at end of file diff --git a/modules/gostats/gostats.go b/modules/gostats/gostats.go index 92fd2ebd4..c52a3b82f 100644 --- a/modules/gostats/gostats.go +++ b/modules/gostats/gostats.go @@ -3,13 +3,13 @@ package main /* #include "lib/layer.h" #include "lib/module.h" -extern int Begin(knot_layer_t *, void *) __asm__ ("main.Begin"); -extern int Finish(knot_layer_t *) __asm__ ("main.Finish"); +int begin(knot_layer_t *, void *); +int finish(knot_layer_t *); static inline const knot_layer_api_t *_layer(void) { static const knot_layer_api_t api = { - .begin = &Begin, - .finish = &Finish + .begin = &begin, + .finish = &finish }; return &api; } @@ -18,29 +18,37 @@ import "C" import "unsafe" import "fmt" -func Api() C.uint32_t { +//export gostats_api +func gostats_api() C.uint32_t { return C.KR_MODULE_API } -func Init(module *C.struct_kr_module) C.int { +//export gostats_init +func gostats_init(module *C.struct_kr_module) int { return 0 } -func Deinit(module *C.struct_kr_module) C.int { +//export gostats_deinit +func gostats_deinit(module *C.struct_kr_module) int { return 0 } -func Begin(ctx *C.knot_layer_t, param unsafe.Pointer) C.int { +//export begin +func begin(ctx *C.knot_layer_t, param unsafe.Pointer) C.int { ctx.data = param return 0 } -func Finish(ctx *C.knot_layer_t) C.int { +//export finish +func finish(ctx *C.knot_layer_t) C.int { var param *C.struct_kr_request = (*C.struct_kr_request)(ctx.data) - fmt.Printf("[gostats] resolved %d queries", C.list_size(¶m.rplan.resolved)) + fmt.Printf("[gostats] resolved %d queries\n", C.list_size(¶m.rplan.resolved)) return 0 } -func Layer(module *C.struct_kr_module) *C.knot_layer_api_t { +//export gostats_layer +func gostats_layer(module *C.struct_kr_module) *C.knot_layer_api_t { return C._layer() } + +func main() {} \ No newline at end of file diff --git a/modules/modules.mk b/modules/modules.mk index 55ddaaecc..8b7a1652f 100644 --- a/modules/modules.mk +++ b/modules/modules.mk @@ -22,7 +22,7 @@ modules_TARGETS += ketcd \ endif # List of Golang modules -ifeq ($(HAS_gccgo),yes) +ifeq ($(HAS_go),yes) modules_TARGETS += gostats endif @@ -54,17 +54,10 @@ endef # Go target definition define go_target $(1) := $(2)/$(1)$(LIBEXT) -$(1)_OBJS := $(addprefix $(2)/_obj/,_cgo_defun.c _cgo_export.c $(subst /,_,$(2))_$(1).cgo2.c) -$(1)_GOBJS := $(addprefix $(2)/_obj/,_cgo_gotypes.go $(subst /,_,$(2))_$(1).cgo1.go) -$(2)/_obj/_cgo_export.h: $$($(1)_SOURCES) - @$(INSTALL) -d $(2)/_obj - $(call quiet,CGO,$$^) -gccgo=true -objdir=$(2)/_obj -- $(BUILD_CFLAGS) $$^ -$(2)/$(1).o: $(2)/_obj/_cgo_export.h - $(call quiet,GCCGO,$$@) -I$(2)/_obj -c -fPIC $$($(1)_GOBJS) -o $$@ -$(2)/$(1)$(LIBEXT): $(2)/$(1).o $$($(1)_DEPEND) - $(call quiet,GCCGO,$$@) -g -fPIC $(BUILD_CFLAGS) -I$(2)/_obj $(2)/$(1).o $$($(1)_OBJS) -o $$@ -$(LIBTYPE) -lgcc -lgo $$($(1)_LIBS) +$(2)/$(1)$(LIBEXT): $$($(1)_SOURCES) $$($(1)_DEPEND) + @echo " GO $(2)"; CGO_CFLAGS="$(BUILD_CFLAGS)" CGO_LDFLAGS="$$($(1)_LIBS)" $(GO) build -buildmode=c-shared -o $$@ $$($(1)_SOURCES) $(1)-clean: - $(RM) -r $(2)/_obj $(2)/$(1)$(LIBEXT) + $(RM) -r $(2)/$(1).h $(2)/$(1)$(LIBEXT) $(1)-install: $(2)/$(1)$(LIBEXT) $(INSTALL) -d $(PREFIX)/$(MODULEDIR) $(INSTALL) $$^ $(PREFIX)/$(MODULEDIR) diff --git a/platform.mk b/platform.mk index ef46719b2..be3ce282c 100644 --- a/platform.mk +++ b/platform.mk @@ -1,7 +1,7 @@ # Platform-specific CCLD := $(CC) CGO := go tool cgo -GCCGO := gccgo +GO := go LIBEXT := .so MODEXT := $(LIBEXT) AREXT := .a @@ -71,7 +71,7 @@ endif endef # Make targets (name,path) -make_bin = $(call make_target,$(1),$(2),$(BINEXT),,$(BINDIR)) +make_bin = $(call make_target,$(1),$(2),$(BINEXT),$(BINFLAGS),$(BINDIR)) make_lib = $(call make_target,$(1),$(2),$(LIBEXT),-$(LIBTYPE),$(LIBDIR)) make_module = $(call make_target,$(1),$(2),$(LIBEXT),-$(LIBTYPE),$(MODULEDIR)) make_shared = $(call make_target,$(1),$(2),$(MODEXT),-$(MODTYPE),$(LIBDIR)) @@ -117,3 +117,13 @@ define find_bin $(1) := $$($(1)_BIN) endif endef + +# Find version +define find_ver + ifeq ($(shell test $(2) -gt $(3); echo $$?),0) + HAS_$(1) := yes + else + HAS_$(1) := no + endif +endef + diff --git a/tests/mock_gomodule.c b/tests/mock_gomodule.c deleted file mode 100644 index c77ea93a1..000000000 --- a/tests/mock_gomodule.c +++ /dev/null @@ -1,44 +0,0 @@ -/* Copyright (C) 2015 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include "lib/module.h" - -/* Fake libgo */ - -void runtime_check(void) {} -void runtime_args(int argc, void *argv) {} -void runtime_osinit(void) {} -void runtime_schedinit(void) {} -void __go_init_main() {} - -/* - * No module implementation. - */ - -/* @note Renamed to mimick Go module. */ -#if defined(__APPLE__) - extern uint32_t Api(void) __asm__ ("_main.Api"); /* Mach-O */ -#elif _WIN32 - #error DLL format is not supported for Golang modules. -#else - extern uint32_t Api(void) __asm__ ("main.Api"); /* ELF */ -#endif - - -uint32_t Api(void) -{ - return KR_MODULE_API - 1; /* Bad version */ -} \ No newline at end of file diff --git a/tests/test_module.c b/tests/test_module.c index 031000e96..cd00da3ca 100644 --- a/tests/test_module.c +++ b/tests/test_module.c @@ -39,20 +39,12 @@ static void test_module_c(void **state) kr_module_unload(&module); } -static void test_module_go(void **state) -{ - /* Mock Go module fails on version check. */ - struct kr_module module; - assert_int_equal(kr_module_load(&module, "mock_gomodule", "tests"), kr_error(ENOTSUP)); -} - int main(void) { const UnitTest tests[] = { unit_test(test_module_params), unit_test(test_module_builtin), unit_test(test_module_c), - unit_test(test_module_go) }; return run_tests(tests); diff --git a/tests/unit.mk b/tests/unit.mk index 6893c5b96..98967130b 100644 --- a/tests/unit.mk +++ b/tests/unit.mk @@ -16,8 +16,6 @@ tests_BIN := \ mock_cmodule_SOURCES := tests/mock_cmodule.c $(eval $(call make_lib,mock_cmodule,tests)) -mock_gomodule_SOURCES := tests/mock_gomodule.c -$(eval $(call make_lib,mock_gomodule,tests)) # Dependencies tests_DEPEND := $(libkres) $(mock_cmodule) $(mock_gomodule) -- GitLab