rust: Add wrapper for cbindgen

This is pretty minimal, but does work at least for basic tests. It also
handles structured_sources.

Has been tested against Mesa's use of cbindgen.

Fixes: #13092
This commit is contained in:
Dylan Baker
2024-04-15 10:54:47 -07:00
committed by Paolo Bonzini
parent da76d46d9b
commit 6a04534897
10 changed files with 466 additions and 3 deletions

View File

@@ -143,6 +143,45 @@ were never turned on by Meson.
bindgen_clang_arguments = ['--target', 'x86_64-linux-gnu']
```
## cbindgen()
*Since 1.12.0*
```meson
file_h = rustmod.cbindgen('infile.rs', 'outfile.h', config : 'cbindgen.toml')
```
This function wraps cbindgen to simplify creating C bindings for Rust targets.
It has several advantages over invoking a `custom_target` directly:
- Correctly handles depfiles, adding them when supported
- Sets the profile to `debug` when Meson is configured with -Ddebug=true
- Automatically passes some useful options for use in Meson, such as `--quiet`
and `--cpp-compat`
- Handles structured_sources correctly
- Automatically detect language based on file extension (c or c++ only),
The majority of the configuration should be handled inside the `config.toml`
file.
It takes the following positional arguments:
- `infile`: the root rust file to bind, or a [[@structured_src]] instance
- `outfile`: the header to generate
It takes the following keyword arguments
- `config`: The Path to a configuration toml. May be a File, str, or CustomTarget.
Customarily, this is called `cbindgen.toml`, but Meson does not require this.
- `language`: The language to bind for. If unset, Meson will choose a language
based on the file extension of the output file. Currently, auto detection only
works for C and C++. May be one of: `c`, `cpp`, `cython`.
- `depends`: An array of CustomTargets that this target depends on
- `depend_files`: An array of files that this target depends on
- `install`: A boolean controlling whether to install the generated header
- `install_dir`: Where to install the header. This is required if `install` is true.
### compiler_target()
*Since 1.11.0*

View File

@@ -0,0 +1,5 @@
## The Rust module includes a basic wrapper for cbindgen
This will correctly track the config.toml, and generate a rust source from the
marked C files. This handles structured_sources correctly, and also handles
depfile generation transparently.

View File

@@ -21,8 +21,8 @@ from ..dependencies import Dependency
from ..interpreter.decorators import apply_machine_map
from ..interpreter.type_checking import (
DEPENDENCIES_KW, LINK_WITH_KW, LINK_WHOLE_KW, SHARED_LIB_KWS, TEST_KWS, TEST_KWS_NO_ARGS,
NATIVE_KW, OUTPUT_KW, INCLUDE_DIRECTORIES, SOURCES_VARARGS, NoneType, in_set_validator,
EXECUTABLE_KWS, LIBRARY_KWS, SHARED_MOD_KWS, _BASE_LANG_KW
OUTPUT_KW, INCLUDE_DIRECTORIES, SOURCES_VARARGS, NATIVE_KW, NoneType, in_set_validator,
EXECUTABLE_KWS, LIBRARY_KWS, SHARED_MOD_KWS, _BASE_LANG_KW, DEPEND_FILES_KW, INSTALL_DIR_KW, INSTALL_KW,
)
from ..interpreterbase import ContainerTypeInfo, InterpreterException, KwargInfo, typed_kwargs, typed_pos_args, noKwargs, noPosargs
from ..interpreter.interpreterobjects import Doctest
@@ -45,8 +45,9 @@ if T.TYPE_CHECKING:
from ..interpreterbase import TYPE_kwargs
from ..programs import Program
from ..interpreter.type_checking import SourcesVarargsType
from ..utils.universal import FileOrString
from typing_extensions import TypedDict, Literal
from typing_extensions import Literal, TypedDict
ArgsType = T.TypeVar('ArgsType')
@@ -73,6 +74,15 @@ if T.TYPE_CHECKING:
language: T.Optional[Literal['c', 'cpp']]
bindgen_version: T.List[str]
class FuncCBindgen(TypedDict):
config: str | File | CustomTarget | CustomTargetIndex
language: T.Literal['c', 'cpp', 'cython'] | None
depends: list[TargetDepends]
depend_files: list[FileOrString]
install: bool
install_dir: str | None
class FuncSubproject(TypedDict):
native: MachineChoice
@@ -583,6 +593,12 @@ class RustSubproject(RustCrate):
return state.overridden_dependency(depname, for_machine=self.for_machine)
def _cbindgen_config_validator(val: str) -> T.Optional[str]:
if os.path.splitext(val)[1] != '.toml':
return 'config file must be a .toml file'
return None
class RustModule(ExtensionModule):
"""A module that holds helper functions for rust."""
@@ -600,6 +616,10 @@ class RustModule(ExtensionModule):
else:
self._bindgen_rust_target = None
self._bindgen_set_std = False
self._cbindgen_bin: Program | None = None
self._cbindgen_has_depfile = False
self.methods.update({
'test': self.test,
'doctest': self.doctest,
@@ -608,6 +628,7 @@ class RustModule(ExtensionModule):
'proc_macro': self.proc_macro,
'to_system_dependency': self.to_system_dependency,
'workspace': self.workspace,
'cbindgen': self.cbindgen,
})
def test_common(self, funcname: str, state: ModuleState, args: T.Tuple[str, BuildTarget], kwargs: FuncRustTest) -> T.Tuple[Executable, _kwargs.FuncTest]:
@@ -1100,6 +1121,121 @@ class RustModule(ExtensionModule):
return RustWorkspace(self.interpreter, ws)
@FeatureNew('rust.cbindgen', '1.12.0')
@typed_pos_args('rust.cbindgen', (str, File, CustomTargetIndex, CustomTarget, StructuredSources), str)
@typed_kwargs(
'rust.cbindgen',
KwargInfo('config', (str, File, CustomTarget, CustomTargetIndex), required=True, validator=_cbindgen_config_validator),
KwargInfo(
'language',
(str, NoneType),
validator=in_set_validator({'c', 'cpp', 'cython'}),
),
KwargInfo(
'depends',
ContainerTypeInfo(list, (CustomTarget, CustomTargetIndex)),
default=[],
listify=True,
),
DEPEND_FILES_KW,
INSTALL_KW,
INSTALL_DIR_KW,
)
def cbindgen(self, state: ModuleState,
args: tuple[FileOrString | CustomTarget | CustomTargetIndex | StructuredSources, str],
kwargs: FuncCBindgen) -> ModuleReturnValue:
# TODO: should we allow GeneratedList here?
if kwargs['install'] and not kwargs['install_dir']:
raise InterpreterException.from_node('cbindgen: When `install` is true `install_dir` must be set',
node=state.current_node)
if self._cbindgen_bin is None:
self._cbindgen_bin = state.find_program('cbindgen')
self._cbindgen_has_depfile = mesonlib.version_compare(
self._cbindgen_bin.get_version(self.interpreter), '>= 0.25')
_infile, outfile = args
infile = self.interpreter.source_strings_to_files([_infile])[0]
depend_files = self.interpreter.source_strings_to_files(kwargs['depend_files'])
depends = kwargs['depends'].copy()
if isinstance(infile, StructuredSources):
infile, *_depends = infile.as_list()
if isinstance(infile, GeneratedList):
raise MesonException.from_node(
'Using a GeneratedList as the main input for cbindgen is unsupported',
node=state.current_node)
for d in _depends:
if isinstance(d, File):
depend_files.append(d)
else:
depends.append(d)
if isinstance(infile, File):
name = infile.fname
else:
if len(infile.get_outputs()) != 1:
raise mesonlib.MesonException.from_node(
'Cannot pass a custom_target generating more than one output to rust.cbindgen, use custom_target[index] to select the output to generate bindings for',
node=state.current_node)
name = infile.get_outputs()[0]
if os.path.dirname(outfile):
raise InvalidArguments.from_node(
'outfile name must not contain a path segment', node=state.current_node)
# Detect langauge from output file extension
language = kwargs['language']
if language is None:
ext = os.path.splitext(outfile)[1][1:]
if ext in lang_suffixes['cpp']:
language = 'cpp'
elif ext == 'h':
language = 'c'
else:
raise InterpreterException.from_node(
f'Unknown file type extension for: {outfile}', node=state.current_node)
# Convert Meson's `cpp` to cbindgen's `c++`
if language == 'cpp':
language = 'c++'
# Set the --profile flag based on meson's debug option
debug = state.get_option('debug')
assert isinstance(debug, bool), 'for mypy'
command: list[str | Program] = [
self._cbindgen_bin, '--output', '@OUTPUT@', '--config',
'@INPUT0@', '--quiet', '--lang', language,
'--profile', 'debug' if debug else 'release',
]
if language == 'c':
command.append('--cpp-compat')
if self._cbindgen_has_depfile:
command.extend(['--depfile', '@DEPFILE@'])
command.extend(['--', '@INPUT1@']) # must be last
config_file = self.interpreter.source_strings_to_files([kwargs['config']])
target = CustomTarget(
f'rustmod-cbindgen-{outfile}',
state.subdir,
state.environment,
command,
[config_file[0], infile],
[outfile],
state.current_build_project,
depfile=f'{name}.d',
depend_files=depend_files,
extra_depends=depends,
install=kwargs['install'],
install_dir=[kwargs['install_dir']],
install_tag=['devel'],
)
return ModuleReturnValue(target, [target])
def initialize(interp: Interpreter) -> RustModule:
return RustModule(interp)

View File

@@ -0,0 +1,159 @@
# This is a template cbindgen.toml file with all of the default values.
# Some values are commented out because their absence is the real default.
#
# See https://github.com/mozilla/cbindgen/blob/master/docs.md#cbindgentoml
# for detailed documentation of every option here.
language = "C++"
############## Options for Wrapping the Contents of the Header #################
# header = "/* Text to put at the beginning of the generated file. Probably a license. */"
# trailer = "/* Text to put at the end of the generated file */"
# include_guard = "my_bindings_h"
pragma_once = true
# autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
include_version = false
# namespace = "my_namespace"
namespaces = []
using_namespaces = []
sys_includes = []
includes = []
no_includes = false
# cpp_compat = true
after_includes = ""
############################ Code Style Options ################################
braces = "SameLine"
line_length = 100
tab_width = 2
documentation = true
documentation_style = "auto"
documentation_length = "full"
line_endings = "LF" # also "CR", "CRLF", "Native"
############################# Codegen Options ##################################
style = "both"
sort_by = "Name" # default for `fn.sort_by` and `const.sort_by`
usize_is_size_t = true
[defines]
# "target_os = freebsd" = "DEFINE_FREEBSD"
# "feature = serde" = "DEFINE_SERDE"
[export]
include = []
exclude = []
# prefix = "CAPI_"
item_types = []
renaming_overrides_prefixing = false
[export.rename]
[export.body]
[export.mangle]
[fn]
rename_args = "None"
# must_use = "MUST_USE_FUNC"
# deprecated = "DEPRECATED_FUNC"
# deprecated_with_note = "DEPRECATED_FUNC_WITH_NOTE"
# no_return = "NO_RETURN"
# prefix = "START_FUNC"
# postfix = "END_FUNC"
args = "auto"
sort_by = "Name"
[struct]
rename_fields = "None"
# must_use = "MUST_USE_STRUCT"
# deprecated = "DEPRECATED_STRUCT"
# deprecated_with_note = "DEPRECATED_STRUCT_WITH_NOTE"
derive_constructor = false
derive_eq = false
derive_neq = false
derive_lt = false
derive_lte = false
derive_gt = false
derive_gte = false
[enum]
rename_variants = "None"
# must_use = "MUST_USE_ENUM"
# deprecated = "DEPRECATED_ENUM"
# deprecated_with_note = "DEPRECATED_ENUM_WITH_NOTE"
add_sentinel = false
prefix_with_name = false
derive_helper_methods = false
derive_const_casts = false
derive_mut_casts = false
# cast_assert_name = "ASSERT"
derive_tagged_enum_destructor = false
derive_tagged_enum_copy_constructor = false
enum_class = true
private_default_tagged_enum_constructor = false
[const]
allow_static_const = true
allow_constexpr = false
sort_by = "Name"
[macro_expansion]
bitflags = false
############## Options for How Your Rust library Should Be Parsed ##############
[parse]
parse_deps = false
# include = []
exclude = []
clean = false
extra_bindings = []
[parse.expand]
crates = []
all_features = false
default_features = true
features = []

View File

@@ -0,0 +1,15 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright © 2024 Intel Corporation
*/
#[repr(C)]
pub struct MyStruct {
pub cost: i64,
pub power: u8,
}
#[no_mangle]
pub extern "C" fn print(x: &MyStruct) {
println!("A thing has a cost of {} and a power of {}", x.cost, x.power);
}

View File

@@ -0,0 +1,12 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright © 2024 Intel Corporation
*/
mod sub;
use sub::MyStruct;
#[no_mangle]
pub extern "C" fn print(x: &MyStruct) {
println!("A thing has a cost of {} and a power of {}", x.cost, x.power);
}

View File

@@ -0,0 +1,51 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright © 2024 Intel Corporation
project(
'cbindgen',
'rust', 'c',
meson_version : '>= 1.12',
default_options : {'cpp_std' : 'c++20'},
)
if not find_program('cbindgen', required : false).found()
error('MESON_SKIP_TEST: cbindgen was not found')
endif
rust = import('rust')
rlib = static_library('rlib', 'lib.rs', rust_abi : 'c')
bound_h = rust.cbindgen('lib.rs', 'lib.h', config : 'config.toml')
test(
'uses bound rust',
executable('ctest', 'test1.c', bound_h, link_with : rlib)
)
srcs = structured_sources(
'lib2.rs',
{
'sub' : 'sub.rs',
}
)
rlib2 = static_library('lib2', srcs, rust_abi : 'c')
bound_h = rust.cbindgen(srcs, 'lib2.h', config : 'config.toml')
test(
'uses bound rust from structured_sources',
executable('ctest2', 'test1.c', bound_h, c_args : '-DUSE_LIB2', link_with : rlib2)
)
# The requirement of C++20 for designated initializers means this may not be
# supported everywhere
if add_languages('cpp', native : false, required : false)
rlib3 = static_library('lib3', 'lib.rs', rust_abi : 'c')
bound_h = rust.cbindgen('lib.rs', 'lib3.hpp', config : 'config.toml')
test(
'generates a c++ header',
executable('cpptest1', 'test1.cpp', bound_h, link_with : rlib2)
)
endif

View File

@@ -0,0 +1,10 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright © 2024 Intel Corporation
*/
#[repr(C)]
pub struct MyStruct {
pub cost: i64,
pub power: u8,
}

View File

@@ -0,0 +1,20 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright © 2024 Intel Corporation
*/
#ifdef USE_LIB2
#include "lib2.h"
#else
#include "lib.h"
#endif
static MyStruct thing = {
.cost = -5,
.power = 1,
};
int main() {
print(&thing);
return 0;
}

View File

@@ -0,0 +1,16 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright © 2024 Intel Corporation
*/
#include "lib3.hpp"
static MyStruct thing = {
.cost = -5,
.power = 1,
};
int main() {
print(&thing);
return 0;
}