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:
Xavier Claessens
2022-03-28 10:59:01 -04:00
committed by Xavier Claessens
parent f3aaebde40
commit a4444c21f3
20 changed files with 328 additions and 7 deletions

View 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.

View 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).

View File

@@ -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

View File

@@ -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")]:

View File

@@ -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]

View File

@@ -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')

View 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()

View File

@@ -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] = []

View File

@@ -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',

View File

@@ -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]

View File

@@ -0,0 +1,3 @@
#include <mylib/lib-static-only.h>
int main(void) { return do_stuff(); }

View File

@@ -0,0 +1,3 @@
#include <mylib/lib.h>
int main(void) { return do_stuff(); }

View 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)

View File

@@ -0,0 +1,5 @@
project('my lib', 'c')
pkg = import('pkgconfig')
subdir('mylib')

View File

@@ -0,0 +1,3 @@
#include "lib-static-only.h"
int do_stuff(void) { return 0; }

View File

@@ -0,0 +1,3 @@
#include <mylib/apiconfig-static-only.h>
MY_LIB_API int do_stuff(void);

View File

@@ -0,0 +1,3 @@
#include "lib.h"
int do_stuff(void) { return 0; }

View File

@@ -0,0 +1,3 @@
#include <mylib/apiconfig.h>
MY_LIB_API int do_stuff(void);

View File

@@ -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)

View 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"}
]
}