diff options
20 files changed, 328 insertions, 7 deletions
diff --git a/docs/markdown/Snippets-module.md b/docs/markdown/Snippets-module.md new file mode 100644 index 000000000..15d5e9f74 --- /dev/null +++ b/docs/markdown/Snippets-module.md @@ -0,0 +1,111 @@ +--- +short-description: Code snippets module +... + +# Snippets module + +*(new in 1.10.0)* + +This module provides helpers to generate commonly useful code snippets. + +## Functions + +### symbol_visibility_header() + +```meson +snippets.symbol_visibility_header(header_name, + namespace: str + api: str + compilation: str + static_compilation: str + static_only: bool +) +``` + +Generate a header file that defines macros to be used to mark all public APIs +of a library. Depending on the platform, this will typically use +`__declspec(dllexport)`, `__declspec(dllimport)` or +`__attribute__((visibility("default")))`. It is compatible with C, C++, +ObjC and ObjC++ languages. The content of the header is static regardless +of the compiler used. + +The first positional argument is the name of the header file to be generated. +It also takes the following keyword arguments: + +- `namespace`: Prefix for generated macros, defaults to the current project name. + It will be converted to upper case with all non-alphanumeric characters replaced + by an underscore `_`. It is only used for `api`, `compilation` and + `static_compilation` default values. +- `api`: Name of the macro used to mark public APIs. Defaults to `<NAMESPACE>_API`. +- `compilation`: Name of a macro defined only when compiling the library. + Defaults to `<NAMESPACE>_COMPILATION`. +- `static_compilation`: Name of a macro defined only when compiling or using + static library. Defaults to `<NAMESPACE>_STATIC_COMPILATION`. +- `static_only`: If set to true, `<NAMESPACE>_STATIC_COMPILATION` is defined + inside the generated header. In that case the header can only be used for + building a static library. By default it is `true` when `default_library=static`, + and `false` otherwise. [See below for more information](#static_library) + +Projects that define multiple shared libraries should typically have +one header per library, with a different namespace. + +The generated header file should be installed using `install_headers()`. + +`meson.build`: +```meson +project('mylib', 'c') +subdir('mylib') +``` + +`mylib/meson.build`: +```meson +snippets = import('snippets') +apiheader = snippets.symbol_visibility_header('apiconfig.h') +install_headers(apiheader, 'lib.h', subdir: 'mylib') +lib = library('mylib', 'lib.c', + gnu_symbol_visibility: 'hidden', + c_args: ['-DMYLIB_COMPILATION'], +) +``` + +`mylib/lib.h` +```c +#include <mylib/apiconfig.h> +MYLIB_API int do_stuff(); +``` + +`mylib/lib.c` +```c +#include "lib.h" +int do_stuff() { + return 0; +} +``` + +#### Static library + +When building both static and shared libraries on Windows (`default_library=both`), +`-D<NAMESPACE>_STATIC_COMPILATION` must be defined only for the static library, +using `c_static_args`. This causes Meson to compile the library twice. + +```meson +if host_system == 'windows' + static_arg = ['-DMYLIB_STATIC_COMPILATION'] +else + static_arg = [] +endif +lib = library('mylib', 'lib.c', + gnu_symbol_visibility: 'hidden', + c_args: ['-DMYLIB_COMPILATION'], + c_static_args: static_arg +) +``` + +`-D<NAMESPACE>_STATIC_COMPILATION` C-flag must be defined when compiling +applications that use the library API. It typically should be defined in +`declare_dependency(..., compile_args: [])` and +`pkgconfig.generate(..., extra_cflags: [])`. + +Note that when building both shared and static libraries on Windows, +applications cannot currently rely on `pkg-config` to define this macro. +See https://github.com/mesonbuild/meson/pull/14829. diff --git a/docs/markdown/snippets/symbol_visibility_header.md b/docs/markdown/snippets/symbol_visibility_header.md new file mode 100644 index 000000000..75aae786d --- /dev/null +++ b/docs/markdown/snippets/symbol_visibility_header.md @@ -0,0 +1,10 @@ +## New method to handle GNU and Windows symbol visibility for C/C++/ObjC/ObjC++ + +Defining public API of a cross platform C/C++/ObjC/ObjC++ library is often +painful and requires copying macro snippets into every projects, typically using +`__declspec(dllexport)`, `__declspec(dllimport)` or +`__attribute__((visibility("default")))`. + +Meson can now generate a header file that defines exactly what's needed for +all supported platforms: +[`snippets.symbol_visibility_header()`](Snippets-module.md#symbol_visibility_header). diff --git a/docs/sitemap.txt b/docs/sitemap.txt index b3967a27f..fa4768746 100644 --- a/docs/sitemap.txt +++ b/docs/sitemap.txt @@ -56,6 +56,7 @@ index.md Qt6-module.md Rust-module.md Simd-module.md + Snippets-module.md SourceSet-module.md Windows-module.md i18n-module.md diff --git a/docs/theme/extra/templates/navbar_links.html b/docs/theme/extra/templates/navbar_links.html index f16489896..ac4a6fe10 100644 --- a/docs/theme/extra/templates/navbar_links.html +++ b/docs/theme/extra/templates/navbar_links.html @@ -26,6 +26,7 @@ ("Qt6-module.html","Qt6"), \ ("Rust-module.html","Rust"), \ ("Simd-module.html","Simd"), \ + ("Snippets-module.html","Snippets"), \ ("SourceSet-module.html","SourceSet"), \ ("Wayland-module.html","Wayland"), \ ("Windows-module.html","Windows")]: diff --git a/docs/yaml/functions/_build_target_base.yaml b/docs/yaml/functions/_build_target_base.yaml index e0fd8b839..4cd91affe 100644 --- a/docs/yaml/functions/_build_target_base.yaml +++ b/docs/yaml/functions/_build_target_base.yaml @@ -255,7 +255,9 @@ kwargs: `default`, `internal`, `hidden`, `protected` or `inlineshidden`, which is the same as `hidden` but also includes things like C++ implicit constructors as specified in the GCC manual. Ignored on compilers that - do not support GNU visibility arguments. + do not support GNU visibility arguments. See also + [`snippets.symbol_visibility_header()`](Snippets-module.md#symbol_visibility_header) + method to help with defining public API. d_import_dirs: type: array[inc | str] diff --git a/mesonbuild/interpreter/primitives/string.py b/mesonbuild/interpreter/primitives/string.py index 49dd71660..df10c568d 100644 --- a/mesonbuild/interpreter/primitives/string.py +++ b/mesonbuild/interpreter/primitives/string.py @@ -7,7 +7,7 @@ import os import typing as T -from ...mesonlib import version_compare, version_compare_many +from ...mesonlib import version_compare, version_compare_many, underscorify from ...interpreterbase import ( InterpreterObject, MesonOperator, @@ -151,7 +151,7 @@ class StringHolder(ObjectHolder[str]): @noPosargs @InterpreterObject.method('underscorify') def underscorify_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str: - return re.sub(r'[^a-zA-Z0-9]', '_', self.held_object) + return underscorify(self.held_object) @noKwargs @InterpreterObject.method('version_compare') diff --git a/mesonbuild/modules/snippets.py b/mesonbuild/modules/snippets.py new file mode 100644 index 000000000..f93a754a8 --- /dev/null +++ b/mesonbuild/modules/snippets.py @@ -0,0 +1,101 @@ +# Copyright 2025 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import textwrap +import typing as T + +from pathlib import Path + +from . import NewExtensionModule, ModuleInfo +from ..interpreterbase import KwargInfo, typed_kwargs, typed_pos_args +from ..interpreter.type_checking import NoneType +from .. import mesonlib + +if T.TYPE_CHECKING: + from typing_extensions import TypedDict + from . import ModuleState + + class SymbolVisibilityHeaderKW(TypedDict): + namespace: T.Optional[str] + api: T.Optional[str] + compilation: T.Optional[str] + static_compilation: T.Optional[str] + static_only: bool + + +class SnippetsModule(NewExtensionModule): + + INFO = ModuleInfo('snippets', '1.10.0') + + def __init__(self) -> None: + super().__init__() + self.methods.update({ + 'symbol_visibility_header': self.symbol_visibility_header_method, + }) + + @typed_kwargs('snippets.symbol_visibility_header', + KwargInfo('namespace', (str, NoneType)), + KwargInfo('api', (str, NoneType)), + KwargInfo('compilation', (str, NoneType)), + KwargInfo('static_compilation', (str, NoneType)), + KwargInfo('static_only', (bool, NoneType))) + @typed_pos_args('snippets.symbol_visibility_header', str) + def symbol_visibility_header_method(self, state: ModuleState, args: T.Tuple[str], kwargs: 'SymbolVisibilityHeaderKW') -> mesonlib.File: + header_name = args[0] + namespace = kwargs['namespace'] or state.project_name + namespace = mesonlib.underscorify(namespace).upper() + if namespace[0].isdigit(): + namespace = f'_{namespace}' + api = kwargs['api'] or f'{namespace}_API' + compilation = kwargs['compilation'] or f'{namespace}_COMPILATION' + static_compilation = kwargs['static_compilation'] or f'{namespace}_STATIC_COMPILATION' + static_only = kwargs['static_only'] + if static_only is None: + default_library = state.get_option('default_library') + static_only = default_library == 'static' + content = textwrap.dedent('''\ + // SPDX-license-identifier: 0BSD OR CC0-1.0 OR WTFPL OR Apache-2.0 OR LGPL-2.0-or-later + #pragma once + ''') + if static_only: + content += textwrap.dedent(f''' + #ifndef {static_compilation} + # define {static_compilation} + #endif /* {static_compilation} */ + ''') + content += textwrap.dedent(f''' + #if (defined(_WIN32) || defined(__CYGWIN__)) && !defined({static_compilation}) + # define {api}_EXPORT __declspec(dllexport) + # define {api}_IMPORT __declspec(dllimport) + #elif __GNUC__ >= 4 + # define {api}_EXPORT __attribute__((visibility("default"))) + # define {api}_IMPORT + #else + # define {api}_EXPORT + # define {api}_IMPORT + #endif + + #ifdef {compilation} + # define {api} {api}_EXPORT extern + #else + # define {api} {api}_IMPORT extern + #endif + ''') + header_path = Path(state.environment.get_build_dir(), state.subdir, header_name) + header_path.write_text(content, encoding='utf-8') + return mesonlib.File.from_built_file(state.subdir, header_name) + +def initialize(*args: T.Any, **kwargs: T.Any) -> SnippetsModule: + return SnippetsModule() diff --git a/mesonbuild/utils/universal.py b/mesonbuild/utils/universal.py index e117f862f..d8e80068f 100644 --- a/mesonbuild/utils/universal.py +++ b/mesonbuild/utils/universal.py @@ -152,6 +152,7 @@ __all__ = [ 'set_meson_command', 'split_args', 'stringlistify', + 'underscorify', 'substitute_values', 'substring_is_in_list', 'typeslistify', @@ -1692,6 +1693,8 @@ def typeslistify(item: 'T.Union[_T, T.Sequence[_T]]', def stringlistify(item: T.Union[T.Any, T.Sequence[T.Any]]) -> T.List[str]: return typeslistify(item, str) +def underscorify(item: str) -> str: + return re.sub(r'[^a-zA-Z0-9]', '_', item) def expand_arguments(args: T.Iterable[str]) -> T.Optional[T.List[str]]: expended_args: T.List[str] = [] diff --git a/run_mypy.py b/run_mypy.py index 08f079aa8..eb004c381 100755 --- a/run_mypy.py +++ b/run_mypy.py @@ -70,6 +70,7 @@ modules = [ 'mesonbuild/modules/qt6.py', 'mesonbuild/modules/rust.py', 'mesonbuild/modules/simd.py', + 'mesonbuild/modules/snippets.py', 'mesonbuild/modules/sourceset.py', 'mesonbuild/modules/wayland.py', 'mesonbuild/modules/windows.py', diff --git a/run_project_tests.py b/run_project_tests.py index 7067bfa0b..c92df2b9f 100755 --- a/run_project_tests.py +++ b/run_project_tests.py @@ -79,7 +79,7 @@ ALL_TESTS = ['cmake', 'common', 'native', 'warning-meson', 'failing-meson', 'fai 'keyval', 'platform-osx', 'platform-windows', 'platform-linux', 'platform-android', 'java', 'C#', 'vala', 'cython', 'rust', 'd', 'objective c', 'objective c++', 'fortran', 'swift', 'cuda', 'python3', 'python', 'fpga', 'frameworks', 'nasm', 'wasm', 'wayland', - 'format', + 'format', 'snippets', ] @@ -355,15 +355,15 @@ def setup_commands(optbackend: str) -> None: def platform_fix_name(fname: str, canonical_compiler: str, env: environment.Environment) -> str: if '?lib' in fname: if env.machines.host.is_windows() and canonical_compiler == 'msvc': - fname = re.sub(r'lib/\?lib(.*)\.', r'bin/\1.', fname) + fname = re.sub(r'lib/\?lib(.*)$', r'bin/\1', fname) fname = re.sub(r'/\?lib/', r'/bin/', fname) elif env.machines.host.is_windows(): - fname = re.sub(r'lib/\?lib(.*)\.', r'bin/lib\1.', fname) + fname = re.sub(r'lib/\?lib(.*)$', r'bin/lib\1', fname) fname = re.sub(r'\?lib(.*)\.dll$', r'lib\1.dll', fname) fname = re.sub(r'/\?lib/', r'/bin/', fname) elif env.machines.host.is_cygwin(): fname = re.sub(r'lib/\?lib(.*)\.so$', r'bin/cyg\1.dll', fname) - fname = re.sub(r'lib/\?lib(.*)\.', r'bin/cyg\1.', fname) + fname = re.sub(r'lib/\?lib(.*)$', r'bin/cyg\1', fname) fname = re.sub(r'\?lib(.*)\.dll$', r'cyg\1.dll', fname) fname = re.sub(r'/\?lib/', r'/bin/', fname) else: @@ -1145,6 +1145,7 @@ def detect_tests_to_run(only: T.Dict[str, T.List[str]], use_tmp: bool) -> T.List TestCategory('wasm', 'wasm', shutil.which('emcc') is None or backend is not Backend.ninja), TestCategory('wayland', 'wayland', should_skip_wayland()), TestCategory('format', 'format'), + TestCategory('snippets', 'snippets'), ] categories = [t.category for t in all_tests] diff --git a/test cases/snippets/1 symbol visibility header/main-static-only.c b/test cases/snippets/1 symbol visibility header/main-static-only.c new file mode 100644 index 000000000..26f5b060f --- /dev/null +++ b/test cases/snippets/1 symbol visibility header/main-static-only.c @@ -0,0 +1,3 @@ +#include <mylib/lib-static-only.h> + +int main(void) { return do_stuff(); } diff --git a/test cases/snippets/1 symbol visibility header/main.c b/test cases/snippets/1 symbol visibility header/main.c new file mode 100644 index 000000000..1d32f622e --- /dev/null +++ b/test cases/snippets/1 symbol visibility header/main.c @@ -0,0 +1,3 @@ +#include <mylib/lib.h> + +int main(void) { return do_stuff(); } diff --git a/test cases/snippets/1 symbol visibility header/meson.build b/test cases/snippets/1 symbol visibility header/meson.build new file mode 100644 index 000000000..9a6c27ae3 --- /dev/null +++ b/test cases/snippets/1 symbol visibility header/meson.build @@ -0,0 +1,13 @@ +project('symbol visibility header', 'c') + +sta_dep = dependency('mylib-sta', fallback: 'sub') +exe = executable('exe-sta', 'main.c', dependencies: sta_dep) +test('test-sta', exe) + +sha_dep = dependency('mylib-sha', fallback: 'sub') +exe = executable('exe-sha', 'main.c', dependencies: sha_dep) +test('test-sha', exe) + +static_only_dep = dependency('static-only', fallback: 'sub') +exe = executable('exe-static-only', 'main-static-only.c', dependencies: static_only_dep) +test('test-static-only', exe) diff --git a/test cases/snippets/1 symbol visibility header/subprojects/sub/meson.build b/test cases/snippets/1 symbol visibility header/subprojects/sub/meson.build new file mode 100644 index 000000000..83b797019 --- /dev/null +++ b/test cases/snippets/1 symbol visibility header/subprojects/sub/meson.build @@ -0,0 +1,5 @@ +project('my lib', 'c') + +pkg = import('pkgconfig') + +subdir('mylib') diff --git a/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/lib-static-only.c b/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/lib-static-only.c new file mode 100644 index 000000000..b7662cd94 --- /dev/null +++ b/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/lib-static-only.c @@ -0,0 +1,3 @@ +#include "lib-static-only.h" + +int do_stuff(void) { return 0; } diff --git a/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/lib-static-only.h b/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/lib-static-only.h new file mode 100644 index 000000000..ebe4524a3 --- /dev/null +++ b/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/lib-static-only.h @@ -0,0 +1,3 @@ +#include <mylib/apiconfig-static-only.h> + +MY_LIB_API int do_stuff(void); diff --git a/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/lib.c b/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/lib.c new file mode 100644 index 000000000..117172070 --- /dev/null +++ b/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/lib.c @@ -0,0 +1,3 @@ +#include "lib.h" + +int do_stuff(void) { return 0; } diff --git a/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/lib.h b/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/lib.h new file mode 100644 index 000000000..bea53afb3 --- /dev/null +++ b/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/lib.h @@ -0,0 +1,3 @@ +#include <mylib/apiconfig.h> + +MY_LIB_API int do_stuff(void); diff --git a/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/meson.build b/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/meson.build new file mode 100644 index 000000000..1e7b45e6b --- /dev/null +++ b/test cases/snippets/1 symbol visibility header/subprojects/sub/mylib/meson.build @@ -0,0 +1,39 @@ +snippets = import('snippets') + +lib_incdir = include_directories('..') +lib_args = ['-DMY_LIB_COMPILATION'] +lib_static_args = ['-DMY_LIB_STATIC_COMPILATION'] + +h = snippets.symbol_visibility_header('apiconfig.h') +install_headers(h, 'lib.h', subdir: 'mylib') +mylib = both_libraries('mylib', 'lib.c', + include_directories: lib_incdir, + gnu_symbol_visibility: 'hidden', + c_args: lib_args, + c_static_args: lib_static_args, + install: true) +mylib_sta_dep = declare_dependency(link_with: mylib.get_static_lib(), + include_directories: lib_incdir, + compile_args: lib_static_args) +mylib_sha_dep = declare_dependency(link_with: mylib.get_shared_lib(), + include_directories: lib_incdir) +meson.override_dependency('mylib-sta', mylib_sta_dep) +meson.override_dependency('mylib-sha', mylib_sha_dep) +pkg.generate(mylib, + extra_cflags: lib_static_args, +) + +# When using static_only, we don't need lib_static_args because +# MY_LIB_STATIC_COMPILATION gets defined in the generated header. +h = snippets.symbol_visibility_header('apiconfig-static-only.h', + static_only: true) +install_headers(h, 'lib-static-only.h', subdir: 'mylib') +libstaticonly = static_library('static-only', 'lib-static-only.c', + include_directories: lib_incdir, + gnu_symbol_visibility: 'hidden', + c_args: lib_args, + install: true) +static_only_dep = declare_dependency(link_with: libstaticonly, + include_directories: lib_incdir) +meson.override_dependency('static-only', static_only_dep) +pkg.generate(libstaticonly) diff --git a/test cases/snippets/1 symbol visibility header/test.json b/test cases/snippets/1 symbol visibility header/test.json new file mode 100644 index 000000000..0d0be4ec5 --- /dev/null +++ b/test cases/snippets/1 symbol visibility header/test.json @@ -0,0 +1,15 @@ +{ + "installed": [ + {"type": "file", "file": "usr/include/mylib/apiconfig-static-only.h"}, + {"type": "file", "file": "usr/include/mylib/apiconfig.h"}, + {"type": "file", "file": "usr/include/mylib/lib-static-only.h"}, + {"type": "file", "file": "usr/include/mylib/lib.h"}, + {"type": "file", "file": "usr/lib/libmylib.a"}, + {"type": "expr", "file": "usr/lib/?libmylib?so"}, + {"type": "implib", "file": "usr/lib/libmylib"}, + {"type": "pdb", "file": "usr/bin/mylib"}, + {"type": "file", "file": "usr/lib/libstatic-only.a"}, + {"type": "file", "file": "usr/lib/pkgconfig/mylib.pc"}, + {"type": "file", "file": "usr/lib/pkgconfig/static-only.pc"} + ] +} |
