mirror of
https://github.com/mesonbuild/meson.git
synced 2026-06-24 08:48:03 +00:00
Add snippets.symbol_visibility_header() method
Defining public API in a cross platform library is painful, especially on Windows. Since every library have to define pretty much the same macros, better do it in Meson.
This commit is contained in:
committed by
Xavier Claessens
parent
f3aaebde40
commit
a4444c21f3
111
docs/markdown/Snippets-module.md
Normal file
111
docs/markdown/Snippets-module.md
Normal file
@@ -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.
|
||||
10
docs/markdown/snippets/symbol_visibility_header.md
Normal file
10
docs/markdown/snippets/symbol_visibility_header.md
Normal file
@@ -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).
|
||||
@@ -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
|
||||
|
||||
1
docs/theme/extra/templates/navbar_links.html
vendored
1
docs/theme/extra/templates/navbar_links.html
vendored
@@ -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")]:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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')
|
||||
|
||||
101
mesonbuild/modules/snippets.py
Normal file
101
mesonbuild/modules/snippets.py
Normal file
@@ -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()
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#include <mylib/lib-static-only.h>
|
||||
|
||||
int main(void) { return do_stuff(); }
|
||||
3
test cases/snippets/1 symbol visibility header/main.c
Normal file
3
test cases/snippets/1 symbol visibility header/main.c
Normal file
@@ -0,0 +1,3 @@
|
||||
#include <mylib/lib.h>
|
||||
|
||||
int main(void) { return do_stuff(); }
|
||||
13
test cases/snippets/1 symbol visibility header/meson.build
Normal file
13
test cases/snippets/1 symbol visibility header/meson.build
Normal file
@@ -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)
|
||||
@@ -0,0 +1,5 @@
|
||||
project('my lib', 'c')
|
||||
|
||||
pkg = import('pkgconfig')
|
||||
|
||||
subdir('mylib')
|
||||
@@ -0,0 +1,3 @@
|
||||
#include "lib-static-only.h"
|
||||
|
||||
int do_stuff(void) { return 0; }
|
||||
@@ -0,0 +1,3 @@
|
||||
#include <mylib/apiconfig-static-only.h>
|
||||
|
||||
MY_LIB_API int do_stuff(void);
|
||||
@@ -0,0 +1,3 @@
|
||||
#include "lib.h"
|
||||
|
||||
int do_stuff(void) { return 0; }
|
||||
@@ -0,0 +1,3 @@
|
||||
#include <mylib/apiconfig.h>
|
||||
|
||||
MY_LIB_API int do_stuff(void);
|
||||
@@ -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)
|
||||
15
test cases/snippets/1 symbol visibility header/test.json
Normal file
15
test cases/snippets/1 symbol visibility header/test.json
Normal file
@@ -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"}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user