diff --git a/doc/rst/source/devdocs/custom_supplements.rst b/doc/rst/source/devdocs/custom_supplements.rst new file mode 100644 index 00000000000..e111814afa9 --- /dev/null +++ b/doc/rst/source/devdocs/custom_supplements.rst @@ -0,0 +1,313 @@ +:orphan: + +Custom GMT Supplements --- In-Tree vs Out-of-Tree +================================================= + +.. note:: + + The investigation underlying this document and the document itself + (including the accompanying template files under + :file:`src/custom_supp_templates/`) were produced by Claude Opus 4.7 + (Anthropic), working from the GMT source tree and a real-world + out-of-tree supplement port (MB-System's ``mbsystem.dll``). Treat + the contents as a starting point that has been mechanically verified + against the GMT source as of the date of writing but has not been + human-reviewed for stylistic or policy fit with the GMT project. + Corrections welcome. + +This document describes the two ways to build a GMT supplemental shared +library ("supplement" or "suppl"): the **in-tree** path (your supplement +lives under :file:`gmt/src//` and is built when GMT itself is +built) and the **out-of-tree** path (your supplement is built by an +independent CMake project that links against an already-installed GMT). +Both produce a DLL or shared module that exposes GMT modules via +``gmt --show-modules``, ``gmt --help``, and the external-API key/group +lookup used by Julia, Python, and MATLAB wrappers. + +The accompanying :file:`src/custom_supp_templates/in-tree-template/` +and :file:`src/custom_supp_templates/out-of-tree-template/` directories +contain copy-and-modify starter trees for each case. + +How a GMT supplement actually works +----------------------------------- + +A GMT supplement is just a shared library that the GMT core loads at +runtime through ``dlopen`` / ``LoadLibrary``. GMT discovers libraries +from the ``GMT_CUSTOM_LIBS`` GMT default (or the built-in supplement +path). For each loaded library, GMT looks up symbols **by name**, using +the library **basename** as a prefix. For a supplement DLL named +``mylib``, GMT resolves these five symbols with ``dlsym``: + +.. list-table:: + :widths: 35 65 + :header-rows: 1 + + * - Symbol + - Purpose + * - ``mylib_module_show_all`` + - Pretty-print all modules + purpose for ``gmt --help`` + * - ``mylib_module_list_all`` + - Plain list of modern module names for ``gmt --show-modules`` + * - ``mylib_module_classic_all`` + - Plain list of classic module names for ``gmt --show-classic`` + * - ``mylib_module_keys`` + - Return ``THIS_MODULE_KEYS`` string for an external-API consumer + * - ``mylib_module_group`` + - Return ``THIS_MODULE_LIB`` string for an external-API consumer + +Per individual module ``xxx``, GMT also looks up the actual entry point +``GMT_xxx`` (for example ``GMT_grdbarb``, ``GMT_psbarb``). That function +must be exported from the DLL +(``EXTERN_MSC int GMT_xxx(void *API, int mode, void *args);``). + +The five ``_module_*`` lookup functions all share a per-supplement +table called ``static struct GMT_MODULEINFO modules[]``, populated with +one row per module: + +.. code-block:: c + + struct GMT_MODULEINFO { + const char *mname; /* THIS_MODULE_MODERN_NAME */ + const char *cname; /* THIS_MODULE_CLASSIC_NAME */ + const char *component; /* THIS_MODULE_LIB */ + const char *purpose; /* THIS_MODULE_PURPOSE */ + const char *keys; /* THIS_MODULE_KEYS */ + }; + +The table is the heart of a supplement. Everything else is plumbing +around it. The lookup-function bodies are nearly identical across all +supplements; they just delegate to ``GMT_Show_ModuleInfo`` / +``GMT_Get_ModuleInfo`` from GMT's public API. + +Because the body is boilerplate and the table content is derived +mechanically from ``#define THIS_MODULE_*`` macros in each module's +``.c``, GMT auto-generates both during the build. That generation is +what the in-tree mechanism gives you for free --- and what the +out-of-tree case must reproduce by hand. + +The in-tree mechanism +--------------------- + +Look at :file:`src/windbarbs/` for the canonical, minimal example: two +module sources (``grdbarb.c``, ``psbarb.c``) plus one shared helper +(``windbarb.c``) and a fourteen-line :file:`CMakeLists.txt`. That +``CMakeLists.txt`` declares only the sources --- no ``add_library``, no +glue file, no ``gen_*`` command: + +.. code-block:: cmake + + set (SUPPL_NAME windbarbs) + set (SUPPL_PROGS_SRCS grdbarb.c psbarb.c) + set (SUPPL_LIB_SRCS ${SUPPL_PROGS_SRCS} windbarb.c) + set (SUPPL_EXAMPLE_FILES README.windbarb) + +:file:`src/CMakeLists.txt` does the rest. The loop in that file walks +every supplement directory listed in ``GMT_SUPPL_DIRS`` (which by +default includes ``geodesy gsfml gshhg img mgd77 potential segy seis +spotter x2sys windbarbs`` plus anything the user puts in +``SUPPL_EXTRA_DIRS``) and for each one: + +1. Calls ``add_subdirectory()`` so the supplement's + :file:`CMakeLists.txt` sets ``SUPPL_NAME``, ``SUPPL_PROGS_SRCS``, + ``SUPPL_LIB_SRCS``, optionally ``SUPPL_LIB_NAME``, + ``SUPPL_EXTRA_LIBS``, ``SUPPL_EXTRA_INCLUDES``, ``SUPPL_DLL_RENAME``. +2. Reads those variables back via ``get_subdir_var``. +3. Accumulates per-library source lists into + ``SUPPL__PROGS_SRCS`` and ``SUPPL__LIB_SRCS``. (Multiple + supplement directories can contribute to one library by sharing + ``SUPPL_LIB_NAME``.) +4. For each resulting library: + + - Runs ``gen_gmt_moduleinfo_h`` from + :file:`cmake/modules/GmtGenExtraHeaders.cmake`. The macro greps + every module source for ``THIS_MODULE_MODERN_NAME``, + ``THIS_MODULE_CLASSIC_NAME``, ``THIS_MODULE_LIB``, + ``THIS_MODULE_PURPOSE``, ``THIS_MODULE_KEYS`` and writes + :file:`gmt__moduleinfo.h` (one row per module). + - Runs ``configure_file(gmt_glue.c.in gmt__glue.c)`` substituting + ``@SHARED_LIB_NAME@`` and ``@SHARED_LIB_PURPOSE@``. The generated + ``.c`` ``#include`` s the generated ``.h`` to fill in ``modules[]`` + and defines the five ``_module_*`` entry points as + ``EXTERN_MSC``. + - Builds the shared library / module from the union of accumulated + module sources, library sources, generated glue, and generated + header. + +5. Installs the library into :file:`gmt/plugins/`. + +That is the entire mechanism. The supplement author writes only the +module sources (with the right ``THIS_MODULE_*`` macros at the top) +plus a fourteen-line :file:`CMakeLists.txt`; everything that exposes +the modules to the runtime is generated. + +Adding a custom in-tree supplement +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Create :file:`gmt/src//` containing your ``.c`` sources + plus a :file:`CMakeLists.txt` shaped like + :file:`src/windbarbs/CMakeLists.txt`. See the + :file:`src/custom_supp_templates/in-tree-template/` directory. +2. Edit (or create) :file:`cmake/ConfigUserAdvanced.cmake` and set + ``set (SUPPL_EXTRA_DIRS )``. (Multiple custom supplement + directories can be listed.) +3. Configure and build GMT normally. Your DLL appears under + :file:`gmt/plugins/` alongside the official supplements. +4. Verify with ``gmt --show-modules`` (your module names should appear) + and ``gmt ``. + +The five-symbol contract and the ``THIS_MODULE_*`` macro requirement +described above are non-negotiable in either path. The in-tree +mechanism just satisfies them automatically. + +The out-of-tree mechanism +------------------------- + +A custom supplement built **outside** the GMT source tree (as part of +an independent project --- for example MB-System builds ``mbsystem.dll`` +from its own CMake project that links against an installed GMT) does +**not** get the auto-generated glue and moduleinfo header. The +supplement author has to either: + +(a) Reproduce the generation in the host project's CMake, or +(b) Hand-author :file:`gmt__glue.c` and + :file:`gmt__moduleinfo.h` and treat them as ordinary source + files. + +Option (a) is strongly recommended --- the moduleinfo header should +track the module sources automatically, and hand-editing it is +error-prone. The :file:`src/custom_supp_templates/out-of-tree-template/` +directory shows option (a) end-to-end. + +What out-of-tree projects must provide +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. **Their own copy of** :file:`gmt_glue.c.in`. GMT does not install + :file:`gmt_glue.c.in` to a public location (it lives in the source + tree only). Vendor a copy into your project; you only need to + change ``@SHARED_LIB_NAME@`` and ``@SHARED_LIB_PURPOSE@``. + +2. **Their own moduleinfo generator.** GMT's + :file:`cmake/modules/GmtGenExtraHeaders.cmake` is similarly + source-tree-only and the ``gen_gmt_moduleinfo_h`` macro is tightly + coupled to GMT's own directory layout (it reads from + ``${GMT_SRC}/src/${prog}``). Vendor a standalone CMake ``-P`` + script that does the same regex extraction over your project's + module sources. See + :file:`src/custom_supp_templates/out-of-tree-template/cmake/GenSupplModuleInfo.cmake`. + +3. **A CMakeLists.txt that wires up the generator → glue → library.** + See + :file:`src/custom_supp_templates/out-of-tree-template/CMakeLists.txt`. + +4. **The right library output name.** GMT calls ``dlsym`` using the + **DLL basename**, so the CMake target's ``OUTPUT_NAME`` (and any + ``*_DLL_RENAME``) must match the ``SHARED_LIB_NAME`` you bake into + the glue. If the DLL is :file:`mylib.dll`, the glue must export + ``mylib_module_list_all`` etc. --- mismatch means GMT silently + finds nothing. + +5. **DLL export visibility.** On Windows the entry points need + ``__declspec(dllexport)``. The vendored :file:`gmt_glue.c.in` uses + ``EXTERN_MSC``, which GMT's :file:`declspec.h` flips to + ``dllexport`` when ``LIBRARY_EXPORTS`` is defined. Add + + .. code-block:: cmake + + target_compile_definitions( PRIVATE LIBRARY_EXPORTS) + + to your CMakeLists. Each module's ``GMT_`` entry point + should also be declared ``EXTERN_MSC`` (or you can rely on CMake's + ``CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS``, but the explicit attribute is + cleaner and matches what in-tree supplements do). + +Real-world worked example +~~~~~~~~~~~~~~~~~~~~~~~~~ + +MB-System builds ``mbsystem.dll`` out-of-tree against an installed GMT. +Its :file:`src/gmt/CMakeLists.txt` followed the recipe above (using a +vendored ``MbsysGenModuleInfo.cmake`` and ``gmt_mbsystem_glue.c.in``) +after diagnosing that an earlier hand-written ``mbgmt_module.c`` +exported the wrong symbol names (``gmt_mbgmt_module_show_all`` instead +of ``mbsystem_module_list_all``), which is why ``gmt --show-modules`` +showed nothing and Julia's ``GMT_Encode_Options`` returned null keys. + +The MB-System fix is a close analogue of the +:file:`src/custom_supp_templates/out-of-tree-template/` provided here; +consult that project as a secondary reference if you want to see the +mechanism integrated into a larger codebase. + +Module source requirements (both paths) +--------------------------------------- + +Every module ``.c`` must define these macros near the top, before any +``#include "gmt_dev.h"``: + +.. code-block:: c + + #define THIS_MODULE_CLASSIC_NAME "mymodule" /* required */ + #define THIS_MODULE_MODERN_NAME "mymodule" /* required */ + #define THIS_MODULE_LIB "mylib" /* must equal DLL basename */ + #define THIS_MODULE_PURPOSE "One-line description" + #define THIS_MODULE_KEYS "X}" /* API key string; "" if none */ + #define THIS_MODULE_NEEDS "Jg" /* projection requirements; "" if none */ + #define THIS_MODULE_OPTIONS "->BJKOPRUVXY" /* common options accepted */ + +The legacy ``THIS_MODULE_NAME`` (a single string used by very old GMT5 +ports) is **not** recognised by the modern generator. If you are +porting old code, add explicit ``THIS_MODULE_MODERN_NAME`` and +``THIS_MODULE_CLASSIC_NAME`` (typically both equal to the old name) or +adapt your generator to fall back. The vendored +:file:`src/custom_supp_templates/out-of-tree-template/cmake/GenSupplModuleInfo.cmake` +does the fallback for convenience; the in-tree GMT generator does not. + +Each module must also expose: + +.. code-block:: c + + EXTERN_MSC int GMT_mymodule(void *V_API, int mode, void *args); + +as a defined function (not just a declaration). That is what GMT +dispatches to when the user runs ``gmt mymodule``. + +Loading a custom supplement at runtime +-------------------------------------- + +After building and installing, point GMT at the new library. Either: + +- Set the GMT default ``GMT_CUSTOM_LIBS`` to the absolute path of the + DLL (or to a colon/semicolon separated list of paths), for example + ``gmt set GMT_CUSTOM_LIBS C:/path/to/mylib.dll``. +- Or drop the DLL into GMT's default plugin directory + (:file:`/lib/gmt/plugins/` on Unix, + :file:`/bin/gmt_plugins/` on Windows). + +Verify: + +.. code-block:: sh + + gmt --show-modules # your modules should appear in the list + gmt mymodule --help # your module should run + +If ``gmt --show-modules`` does not list your modules but the DLL +clearly loaded (no error printed), the most likely cause is a mismatch +between the DLL basename and the ``SHARED_LIB_NAME`` baked into the +glue. Inspect the DLL with ``dumpbin /exports mylib.dll`` (MSVC) or +``nm -D mylib.so`` (Unix) and confirm that ``mylib_module_list_all`` +and friends are present. + +File overview in :file:`src/custom_supp_templates/` +--------------------------------------------------- + +.. code-block:: text + + src/custom_supp_templates/ + ├── README.md this document (markdown copy) + ├── in-tree-template/ + │ ├── CMakeLists.txt drop-in for src// + │ └── mymodule.c skeleton module source + └── out-of-tree-template/ + ├── CMakeLists.txt standalone CMake project + ├── gmt_mylib_glue.c.in vendored from gmt_glue.c.in + ├── mymodule.c skeleton module source + └── cmake/ + └── GenSupplModuleInfo.cmake vendored moduleinfo generator diff --git a/doc/rst/source/index.rst b/doc/rst/source/index.rst index 9659557dbce..fce5bdabb1e 100644 --- a/doc/rst/source/index.rst +++ b/doc/rst/source/index.rst @@ -80,6 +80,7 @@ it can do. Debugging GMT GMT C API PostScriptLight C API + Custom Supplements /devdocs/devdocs .. Add a hidden toctree to suppress "document isn't included in any toctree" warnings diff --git a/src/custom_supp_templates/in-tree-template/CMakeLists.txt b/src/custom_supp_templates/in-tree-template/CMakeLists.txt new file mode 100644 index 00000000000..63f42a2eb63 --- /dev/null +++ b/src/custom_supp_templates/in-tree-template/CMakeLists.txt @@ -0,0 +1,61 @@ +# +# In-tree custom supplement template — drop this whole directory into +# gmt/src// and add `set (SUPPL_EXTRA_DIRS )` to your +# cmake/ConfigUserAdvanced.cmake. The top-level GMT CMake will then +# build a separate shared library (DLL on Windows, .so MODULE on Unix) +# and install it next to the official supplements. +# +# See ../README.md for the full mechanism description, and +# gmt/src/windbarbs/CMakeLists.txt for the canonical real-world example. +# +# The variables understood by src/CMakeLists.txt are: +# +# SUPPL_NAME Display name of the supplement package. +# SUPPL_LIB_NAME (optional) Output library/DLL basename. Defaults +# to ${GMT_SUPPL_LIB_NAME} ("supplements"), which +# bundles your modules into the standard supp DLL. +# Set this to a distinct value if you want a +# SEPARATE DLL — then the DLL basename, the +# SHARED_LIB_NAME baked into the auto-generated +# glue, and your THIS_MODULE_LIB strings must all +# agree. +# SUPPL_PROGS_SRCS List of .c files, each defining one module +# with THIS_MODULE_* macros and a GMT_ +# entry point. +# SUPPL_LIB_SRCS SUPPL_PROGS_SRCS plus any helper .c files that +# do not define a module but provide shared code. +# SUPPL_HEADERS (optional) Public headers to install when +# BUILD_DEVELOPER is TRUE. +# SUPPL_EXTRA_INCLUDES (optional) Extra include directories. +# SUPPL_EXTRA_LIBS (optional) Extra libraries to link against. +# SUPPL_DLL_RENAME (optional, Windows) Override the DLL basename. +# If set, this MUST agree with THIS_MODULE_LIB in +# every module source. +# SUPPL_LIB_PURPOSE (optional) One-line description shown by +# `gmt --help` for this supplement. +# SUPPL_EXAMPLE_FILES (optional) README and example files installed +# under share/doc/gmt//. +# SUPPL_EXAMPLE_PROGS (optional) Example scripts installed under +# share/doc/gmt//. +# + +set (SUPPL_NAME mymodules) + +# Uncomment and set if you want a separate DLL named libmymodules.dll +# rather than bundling into the standard supplements library. If you do +# this, every module .c in SUPPL_PROGS_SRCS must use the matching +# `#define THIS_MODULE_LIB "mymodules"`. +#set (SUPPL_LIB_NAME mymodules) +#set (SUPPL_LIB_PURPOSE "Custom GMT modules for ...") + +set (SUPPL_PROGS_SRCS mymodule.c) + +# If you have helper translation units that do NOT define a module +# (no THIS_MODULE_* macros, no GMT_ entry point), list them here +# in addition to SUPPL_PROGS_SRCS. Otherwise mirror SUPPL_PROGS_SRCS. +set (SUPPL_LIB_SRCS ${SUPPL_PROGS_SRCS}) + +#set (SUPPL_EXTRA_INCLUDES ) +#set (SUPPL_EXTRA_LIBS ) +#set (SUPPL_EXAMPLE_FILES README.mymodules) +#set (SUPPL_EXAMPLE_PROGS example_mymodule.sh) diff --git a/src/custom_supp_templates/in-tree-template/mymodule.c b/src/custom_supp_templates/in-tree-template/mymodule.c new file mode 100644 index 00000000000..2e432d9b017 --- /dev/null +++ b/src/custom_supp_templates/in-tree-template/mymodule.c @@ -0,0 +1,105 @@ +/*-------------------------------------------------------------------- + * + * In-tree custom supplement template — module source. + * See ../README.md for the supplement mechanism overview. + * + * Rename this file, the THIS_MODULE_* macros, and the GMT_mymodule + * function below to your real module name. The macro values are read + * by GMT's build-time generator (gen_gmt_moduleinfo_h in + * cmake/modules/GmtGenExtraHeaders.cmake) to populate the supplement's + * module table — they ARE the module's identity at runtime, not just + * documentation. + * + *--------------------------------------------------------------------*/ + +#define THIS_MODULE_CLASSIC_NAME "mymodule" +#define THIS_MODULE_MODERN_NAME "mymodule" +#define THIS_MODULE_LIB "mymodules" /* must match DLL basename */ +#define THIS_MODULE_PURPOSE "One-line description of what this module does" +#define THIS_MODULE_KEYS "" /* see gmt_resources.h for the key syntax */ +#define THIS_MODULE_NEEDS "" /* "Jg" if module needs a projection, etc. */ +#define THIS_MODULE_OPTIONS "-Vh" /* common options accepted via GMT_Parse_Common */ + +#include "gmt_dev.h" + +/* Per-module option-parsing control struct. Replace with the real + * fields you need. */ +struct MYMODULE_CTRL { + struct MYMODULE_A { /* -A */ + bool active; + } A; +}; + +static void *New_Ctrl(struct GMT_CTRL *GMT) { + struct MYMODULE_CTRL *C = gmt_M_memory(GMT, NULL, 1, struct MYMODULE_CTRL); + return C; +} + +static void Free_Ctrl(struct GMT_CTRL *GMT, struct MYMODULE_CTRL *C) { + if (!C) return; + gmt_M_free(GMT, C); +} + +static int usage(struct GMTAPI_CTRL *API, int level) { + const char *name = gmt_show_name_and_purpose(API, THIS_MODULE_LIB, + THIS_MODULE_MODERN_NAME, + THIS_MODULE_PURPOSE); + (void)name; + if (level == GMT_MODULE_PURPOSE) return GMT_NOERROR; + + GMT_Usage(API, 0, "usage: %s [-A] [%s] [%s]\n", THIS_MODULE_MODERN_NAME, + GMT_V_OPT, GMT_PAR_OPT); + if (level == GMT_SYNOPSIS) return GMT_MODULE_SYNOPSIS; + + GMT_Usage(API, 1, "\n-A Example flag."); + GMT_Option(API, "V,."); + return GMT_MODULE_USAGE; +} + +static int parse(struct GMT_CTRL *GMT, struct MYMODULE_CTRL *Ctrl, + struct GMT_OPTION *options) { + unsigned int n_errors = 0; + for (struct GMT_OPTION *opt = options; opt; opt = opt->next) { + switch (opt->option) { + case 'A': + Ctrl->A.active = true; + break; + default: + n_errors += gmt_default_option_error(GMT, opt); + break; + } + } + return n_errors ? GMT_PARSE_ERROR : GMT_NOERROR; +} + +EXTERN_MSC int GMT_mymodule(void *V_API, int mode, void *args) { + struct GMTAPI_CTRL *API = gmt_get_api_ptr(V_API); + struct GMT_OPTION *options = NULL; + struct GMT_CTRL *GMT = NULL, *GMT_cpy = NULL; + struct MYMODULE_CTRL *Ctrl = NULL; + int error = 0; + + if (API == NULL) return GMT_NOT_A_SESSION; + if (mode == GMT_MODULE_PURPOSE) return usage(API, GMT_MODULE_PURPOSE); + options = GMT_Create_Options(API, mode, args); + if (API->error) return API->error; + + if (!options || options->option == GMT_OPT_USAGE) + return usage(API, GMT_USAGE); + if (options->option == GMT_OPT_SYNOPSIS) + return usage(API, GMT_SYNOPSIS); + + GMT = gmt_init_module(API, THIS_MODULE_LIB, THIS_MODULE_CLASSIC_NAME, + THIS_MODULE_KEYS, THIS_MODULE_NEEDS, NULL, + &options, &GMT_cpy); + + Ctrl = New_Ctrl(GMT); + if ((error = parse(GMT, Ctrl, options)) != 0) goto cleanup; + + /* ---- module body goes here ---- */ + +cleanup: + Free_Ctrl(GMT, Ctrl); + gmt_end_module(GMT, GMT_cpy); + return error ? error : GMT_NOERROR; +} diff --git a/src/custom_supp_templates/out-of-tree-template/CMakeLists.txt b/src/custom_supp_templates/out-of-tree-template/CMakeLists.txt new file mode 100644 index 00000000000..2d8ca4eced8 --- /dev/null +++ b/src/custom_supp_templates/out-of-tree-template/CMakeLists.txt @@ -0,0 +1,97 @@ +# +# Out-of-tree custom GMT supplement template. +# See ../README.md for the full mechanism description. +# +# This builds a shared library `mylib` that ships GMT modules and is +# loadable by an already-installed GMT (set GMT_CUSTOM_LIBS to point at +# the built DLL, or copy the DLL into GMT's plugin directory). +# +# To adapt: +# 1. Replace MYLIB_SUPPL_NAME / MYLIB_SUPPL_PURPOSE. +# 2. Rename gmt_mylib_glue.c.in to gmt__glue.c.in (or keep +# and tweak the configure_file source path). +# 3. List your module .c files in MYLIB_MODULE_SRCS. +# 4. Each module .c must define THIS_MODULE_LIB equal to +# MYLIB_SUPPL_NAME, plus THIS_MODULE_MODERN_NAME/CLASSIC_NAME, +# THIS_MODULE_PURPOSE, THIS_MODULE_KEYS. See mymodule.c. +# + +cmake_minimum_required(VERSION 3.16) +project(mylib_suppl C) + +find_package(GMT CONFIG) +if (NOT GMT_FOUND) + message(STATUS "GMTConfig.cmake not found, falling back to FindGMT.cmake") + find_package(GMT REQUIRED) +endif () + +# ---- supplement identity ---------------------------------------------------- +# The DLL name (OUTPUT_NAME below) MUST equal MYLIB_SUPPL_NAME, because +# GMT resolves modules with dlsym("_module__all"). +set(MYLIB_SUPPL_NAME mylib) +set(MYLIB_SUPPL_PURPOSE "Custom GMT modules for ...") + +set(MYLIB_MODULE_SRCS + mymodule.c + # add more module .c files here +) + +# ---- generate moduleinfo header -------------------------------------------- +set(MYLIB_MODULEINFO_H "${CMAKE_CURRENT_BINARY_DIR}/gmt_${MYLIB_SUPPL_NAME}_moduleinfo.h") +set(MYLIB_GLUE_C "${CMAKE_CURRENT_BINARY_DIR}/gmt_${MYLIB_SUPPL_NAME}_glue.c") + +set(_mylib_moduleinfo_inputs) +foreach (_s ${MYLIB_MODULE_SRCS}) + list(APPEND _mylib_moduleinfo_inputs "${CMAKE_CURRENT_SOURCE_DIR}/${_s}") +endforeach () + +add_custom_command( + OUTPUT "${MYLIB_MODULEINFO_H}" + COMMAND ${CMAKE_COMMAND} + -D "SRC_DIR=${CMAKE_CURRENT_SOURCE_DIR}" + -D "OUTPUT_FILE=${MYLIB_MODULEINFO_H}" + -D "PROGS_SRCS=${MYLIB_MODULE_SRCS}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/GenSupplModuleInfo.cmake" + DEPENDS + ${_mylib_moduleinfo_inputs} + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/GenSupplModuleInfo.cmake" + COMMENT "Generating ${MYLIB_SUPPL_NAME} moduleinfo header" + VERBATIM) + +# ---- generate glue translation unit ----------------------------------------- +# These two vars are consumed by gmt_mylib_glue.c.in via configure_file. +set(SHARED_LIB_NAME "${MYLIB_SUPPL_NAME}") +set(SHARED_LIB_PURPOSE "${MYLIB_SUPPL_PURPOSE}") +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/gmt_mylib_glue.c.in" + "${MYLIB_GLUE_C}" + @ONLY) + +# ---- build shared library --------------------------------------------------- +add_library(${MYLIB_SUPPL_NAME} SHARED + ${MYLIB_MODULE_SRCS} + ${MYLIB_GLUE_C} + ${MYLIB_MODULEINFO_H}) + +set_target_properties(${MYLIB_SUPPL_NAME} PROPERTIES + OUTPUT_NAME ${MYLIB_SUPPL_NAME} + PREFIX "" + VERSION 0 + SOVERSION 0) + +# GMT's declspec.h flips EXTERN_MSC to __declspec(dllexport) when +# LIBRARY_EXPORTS is defined. Required so our module entry points and +# the glue's _module_* symbols are exported on Windows. +target_compile_definitions(${MYLIB_SUPPL_NAME} PRIVATE LIBRARY_EXPORTS) + +# The generated glue and moduleinfo header live in the binary dir. +target_include_directories(${MYLIB_SUPPL_NAME} PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" + "${CMAKE_CURRENT_BINARY_DIR}") + +target_link_libraries(${MYLIB_SUPPL_NAME} PRIVATE GMT::GMT GMT::PSL) + +install(TARGETS ${MYLIB_SUPPL_NAME} + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin + ARCHIVE DESTINATION lib) diff --git a/src/custom_supp_templates/out-of-tree-template/cmake/GenSupplModuleInfo.cmake b/src/custom_supp_templates/out-of-tree-template/cmake/GenSupplModuleInfo.cmake new file mode 100644 index 00000000000..8f923445b07 --- /dev/null +++ b/src/custom_supp_templates/out-of-tree-template/cmake/GenSupplModuleInfo.cmake @@ -0,0 +1,94 @@ +# GenSupplModuleInfo.cmake — generate gmt__moduleinfo.h for an +# out-of-tree GMT supplement. +# +# This script is invoked in CMake script mode by add_custom_command in +# the parent CMakeLists.txt: +# +# cmake -P GenSupplModuleInfo.cmake +# -D SRC_DIR= +# -D OUTPUT_FILE=_moduleinfo.h to write> +# -D PROGS_SRCS="" +# +# Output is the body of a `static struct GMT_MODULEINFO modules[]` +# initializer — one row per module — to be #included by the +# gmt__glue.c translation unit. +# +# Adapted from GMT's cmake/modules/GmtGenExtraHeaders.cmake +# (LGPL, GMT Team), with one deliberate addition: if a source has only +# the legacy THIS_MODULE_NAME macro (no MODERN/CLASSIC dual macros), +# fall back to using THIS_MODULE_NAME for both fields. This makes the +# generator usable on projects still being ported from GMT5-era code. +# The in-tree GMT generator does NOT do this fallback; if you want +# strict parity, delete the fallback block and add MODERN/CLASSIC +# macros to every module. + +if (NOT DEFINED OUTPUT_FILE) + message (FATAL_ERROR "GenSupplModuleInfo: OUTPUT_FILE not set") +endif () +if (NOT DEFINED SRC_DIR) + message (FATAL_ERROR "GenSupplModuleInfo: SRC_DIR not set") +endif () +if (NOT DEFINED PROGS_SRCS) + message (FATAL_ERROR "GenSupplModuleInfo: PROGS_SRCS not set") +endif () + +separate_arguments (PROGS_SRCS) + +set (_moduleinfo "") +foreach (_prog_src ${PROGS_SRCS}) + file (READ "${SRC_DIR}/${_prog_src}" _src) + + set (_modern "") + set (_classic "") + set (_legacy "") + set (_lib "") + set (_purpose "") + set (_keys "") + + string (REGEX MATCH "#define[ \t]+THIS_MODULE_MODERN_NAME[ \t]+\"([^\"\n]*)\"" _m "${_src}") + set (_modern "${CMAKE_MATCH_1}") + + string (REGEX MATCH "#define[ \t]+THIS_MODULE_CLASSIC_NAME[ \t]+\"([^\"\n]*)\"" _m "${_src}") + set (_classic "${CMAKE_MATCH_1}") + + string (REGEX MATCH "#define[ \t]+THIS_MODULE_NAME[ \t]+\"([^\"\n]*)\"" _m "${_src}") + set (_legacy "${CMAKE_MATCH_1}") + + string (REGEX MATCH "#define[ \t]+THIS_MODULE_LIB[ \t]+\"([^\"\n]*)\"" _m "${_src}") + set (_lib "${CMAKE_MATCH_1}") + + string (REGEX MATCH "#define[ \t]+THIS_MODULE_PURPOSE[ \t]+\"([^\"\n]*)\"" _m "${_src}") + set (_purpose "${CMAKE_MATCH_1}") + + string (REGEX MATCH "#define[ \t]+THIS_MODULE_KEYS[ \t]+\"([^\"\n]*)\"" _m "${_src}") + set (_keys "${CMAKE_MATCH_1}") + + # Legacy-fallback. Remove this block for in-tree-equivalent strict mode. + if ("${_modern}" STREQUAL "") + set (_modern "${_legacy}") + endif () + if ("${_classic}" STREQUAL "") + set (_classic "${_legacy}") + endif () + + if ("${_modern}" STREQUAL "") + message (WARNING "GenSupplModuleInfo: ${_prog_src} has no THIS_MODULE_*_NAME — skipping") + continue () + endif () + if ("${_lib}" STREQUAL "") + message (WARNING "GenSupplModuleInfo: ${_prog_src} has no THIS_MODULE_LIB — skipping") + continue () + endif () + if ("${_purpose}" STREQUAL "") + message (WARNING "GenSupplModuleInfo: ${_prog_src} has no THIS_MODULE_PURPOSE") + endif () + + set (_row "\t{\"${_modern}\", \"${_classic}\", \"${_lib}\", \"${_purpose}\", \"${_keys}\"},") + if (_moduleinfo) + set (_moduleinfo "${_moduleinfo}\n${_row}") + else () + set (_moduleinfo "${_row}") + endif () +endforeach () + +file (WRITE "${OUTPUT_FILE}" "${_moduleinfo}\n") diff --git a/src/custom_supp_templates/out-of-tree-template/gmt_mylib_glue.c.in b/src/custom_supp_templates/out-of-tree-template/gmt_mylib_glue.c.in new file mode 100644 index 00000000000..a7a1c0deb82 --- /dev/null +++ b/src/custom_supp_templates/out-of-tree-template/gmt_mylib_glue.c.in @@ -0,0 +1,43 @@ +/*-------------------------------------------------------------------- + * + * Out-of-tree custom supplement glue template. + * Vendored from gmt/src/gmt_glue.c.in (LGPL, GMT Team). + * + * This translation unit populates the per-DLL GMT module table that + * `gmt --show-modules`, `gmt --help`, and GMT_Encode_Options query via + * dlsym("_module_*"). The libname is the DLL basename, so + * the exported entry points MUST be named + * _module_show_all / _list_all / _classic_all / _keys / _group. + * + * Two @-tokens are substituted by configure_file in the parent + * CMakeLists.txt: @SHARED_LIB_NAME@ (must equal the DLL basename) and + * @SHARED_LIB_PURPOSE@ (one-line description shown by `gmt --help`). + * + *--------------------------------------------------------------------*/ + +#include "gmt.h" + +static struct GMT_MODULEINFO modules[] = { +#include "gmt_@SHARED_LIB_NAME@_moduleinfo.h" + {NULL, NULL, NULL, NULL, NULL} /* sentinel */ +}; + +EXTERN_MSC int @SHARED_LIB_NAME@_module_show_all (void *API) { + return (GMT_Show_ModuleInfo (API, modules, "@SHARED_LIB_PURPOSE@", GMT_MODULE_HELP)); +} + +EXTERN_MSC int @SHARED_LIB_NAME@_module_list_all (void *API) { + return (GMT_Show_ModuleInfo (API, modules, NULL, GMT_MODULE_SHOW_MODERN)); +} + +EXTERN_MSC int @SHARED_LIB_NAME@_module_classic_all (void *API) { + return (GMT_Show_ModuleInfo (API, modules, NULL, GMT_MODULE_SHOW_CLASSIC)); +} + +EXTERN_MSC const char *@SHARED_LIB_NAME@_module_keys (void *API, char *candidate) { + return (GMT_Get_ModuleInfo (API, modules, candidate, GMT_MODULE_KEYS)); +} + +EXTERN_MSC const char *@SHARED_LIB_NAME@_module_group (void *API, char *candidate) { + return (GMT_Get_ModuleInfo (API, modules, candidate, GMT_MODULE_GROUP)); +} diff --git a/src/custom_supp_templates/out-of-tree-template/mymodule.c b/src/custom_supp_templates/out-of-tree-template/mymodule.c new file mode 100644 index 00000000000..0d7b92af1e1 --- /dev/null +++ b/src/custom_supp_templates/out-of-tree-template/mymodule.c @@ -0,0 +1,102 @@ +/*-------------------------------------------------------------------- + * + * Out-of-tree custom supplement template — module source. + * See ../README.md for the supplement mechanism overview. + * + * Identical in shape to an in-tree module. The only difference is + * THIS_MODULE_LIB must equal the DLL basename produced by the parent + * out-of-tree CMakeLists.txt (MYLIB_SUPPL_NAME). The build-time + * generator (cmake/GenSupplModuleInfo.cmake) reads the macros below + * by regex. + * + *--------------------------------------------------------------------*/ + +#define THIS_MODULE_CLASSIC_NAME "mymodule" +#define THIS_MODULE_MODERN_NAME "mymodule" +#define THIS_MODULE_LIB "mylib" /* must match DLL basename */ +#define THIS_MODULE_PURPOSE "One-line description of what this module does" +#define THIS_MODULE_KEYS "" /* see gmt_resources.h for key syntax */ +#define THIS_MODULE_NEEDS "" /* "Jg" if a projection is required */ +#define THIS_MODULE_OPTIONS "-Vh" /* common options accepted */ + +#include "gmt_dev.h" + +struct MYMODULE_CTRL { + struct MYMODULE_A { /* -A */ + bool active; + } A; +}; + +static void *New_Ctrl(struct GMT_CTRL *GMT) { + struct MYMODULE_CTRL *C = gmt_M_memory(GMT, NULL, 1, struct MYMODULE_CTRL); + return C; +} + +static void Free_Ctrl(struct GMT_CTRL *GMT, struct MYMODULE_CTRL *C) { + if (!C) return; + gmt_M_free(GMT, C); +} + +static int usage(struct GMTAPI_CTRL *API, int level) { + const char *name = gmt_show_name_and_purpose(API, THIS_MODULE_LIB, + THIS_MODULE_MODERN_NAME, + THIS_MODULE_PURPOSE); + (void)name; + if (level == GMT_MODULE_PURPOSE) return GMT_NOERROR; + + GMT_Usage(API, 0, "usage: %s [-A] [%s] [%s]\n", THIS_MODULE_MODERN_NAME, + GMT_V_OPT, GMT_PAR_OPT); + if (level == GMT_SYNOPSIS) return GMT_MODULE_SYNOPSIS; + + GMT_Usage(API, 1, "\n-A Example flag."); + GMT_Option(API, "V,."); + return GMT_MODULE_USAGE; +} + +static int parse(struct GMT_CTRL *GMT, struct MYMODULE_CTRL *Ctrl, + struct GMT_OPTION *options) { + unsigned int n_errors = 0; + for (struct GMT_OPTION *opt = options; opt; opt = opt->next) { + switch (opt->option) { + case 'A': + Ctrl->A.active = true; + break; + default: + n_errors += gmt_default_option_error(GMT, opt); + break; + } + } + return n_errors ? GMT_PARSE_ERROR : GMT_NOERROR; +} + +EXTERN_MSC int GMT_mymodule(void *V_API, int mode, void *args) { + struct GMTAPI_CTRL *API = gmt_get_api_ptr(V_API); + struct GMT_OPTION *options = NULL; + struct GMT_CTRL *GMT = NULL, *GMT_cpy = NULL; + struct MYMODULE_CTRL *Ctrl = NULL; + int error = 0; + + if (API == NULL) return GMT_NOT_A_SESSION; + if (mode == GMT_MODULE_PURPOSE) return usage(API, GMT_MODULE_PURPOSE); + options = GMT_Create_Options(API, mode, args); + if (API->error) return API->error; + + if (!options || options->option == GMT_OPT_USAGE) + return usage(API, GMT_USAGE); + if (options->option == GMT_OPT_SYNOPSIS) + return usage(API, GMT_SYNOPSIS); + + GMT = gmt_init_module(API, THIS_MODULE_LIB, THIS_MODULE_CLASSIC_NAME, + THIS_MODULE_KEYS, THIS_MODULE_NEEDS, NULL, + &options, &GMT_cpy); + + Ctrl = New_Ctrl(GMT); + if ((error = parse(GMT, Ctrl, options)) != 0) goto cleanup; + + /* ---- module body goes here ---- */ + +cleanup: + Free_Ctrl(GMT, Ctrl); + gmt_end_module(GMT, GMT_cpy); + return error ? error : GMT_NOERROR; +}