diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index 9f217ea8b..fdff48310 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -3,7 +3,7 @@ short-description: Rust language integration module authors: - name: Dylan Baker email: dylan@pnwbakers.com - years: [2020] + years: [2020, 2021] ... # Unstable Rust module @@ -33,3 +33,51 @@ that automatically. Additional, test only dependencies may be passed via the dependencies argument. + +### bindgen(*, input: string | BuildTarget | []string | []BuildTarget, output: strng, include_directories: []include_directories, c_args: []string, args: []string) + +This function wraps bindgen to simplify creating rust bindings around C +libraries. This has two advantages over hand-rolling ones own with a +`generator` or `custom_target`: + +- It handles `include_directories`, so one doesn't have to manually convert them to `-I...` +- It automatically sets up a depfile, making the results more reliable + + +It takes the following keyword arguments + +- input — A list of Files, Strings, or CustomTargets. The first element is + the header bindgen will parse, additional elements are dependencies. +- output — the name of the output rust file +- include_directories — A list of `include_directories` objects, these are + passed to clang as `-I` arguments +- c_args — A list of string arguments to pass to clang untouched +- args — A list of string arguments to pass to `bindgen` untouched. + +```meson +rust = import('unstable-rust') + +inc = include_directories('..'¸ '../../foo') + +generated = rust.bindgen( + 'myheader.h', + 'generated.rs', + include_directories : [inc, include_directories('foo'), + args : ['--no-rustfmt-bindings'], + c_args : ['-DFOO=1'], +) +``` + +If the header depeneds on generated headers, those headers must be passed to +`bindgen` as well to ensure proper dependency ordering, static headers do not +need to be passed, as a proper depfile is generated: + +```meson +h1 = custom_target(...) +h2 = custom_target(...) + +r1 = rust.bindgen( + [h1, h2], # h1 includes h2, + 'out.rs', +) +``` diff --git a/docs/markdown/snippets/unstable-rust-module.md b/docs/markdown/snippets/unstable-rust-module.md index e594ecf34..87ffe3306 100644 --- a/docs/markdown/snippets/unstable-rust-module.md +++ b/docs/markdown/snippets/unstable-rust-module.md @@ -1,4 +1,5 @@ ## Unstable Rust module A new unstable module has been added to make using Rust with Meson easier. -Currently it adds a single function to ease defining Rust tests. +Currently, it adds a single function to ease defining Rust tests, as well as a +wrapper around bindgen, making it easier to use. diff --git a/mesonbuild/modules/unstable_rust.py b/mesonbuild/modules/unstable_rust.py index e74c18109..c4d7d41b6 100644 --- a/mesonbuild/modules/unstable_rust.py +++ b/mesonbuild/modules/unstable_rust.py @@ -12,18 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import typing as T from . import ExtensionModule, ModuleReturnValue from .. import mlog -from ..build import BuildTarget, Executable, InvalidArguments +from ..build import BuildTarget, CustomTargetIndex, Executable, GeneratedList, InvalidArguments, IncludeDirs, CustomTarget from ..dependencies import Dependency, ExternalLibrary -from ..interpreter import ExecutableHolder, BuildTargetHolder, permitted_kwargs +from ..interpreter import ExecutableHolder, BuildTargetHolder, CustomTargetHolder, permitted_kwargs, noPosargs from ..interpreterbase import InterpreterException, permittedKwargs, FeatureNew, typed_pos_args -from ..mesonlib import stringlistify, unholder, listify +from ..mesonlib import stringlistify, unholder, listify, typeslistify, File if T.TYPE_CHECKING: from ..interpreter import ModuleState, Interpreter + from ..dependencies import ExternalProgram class RustModule(ExtensionModule): @@ -33,6 +35,7 @@ class RustModule(ExtensionModule): @FeatureNew('rust module', '0.57.0') def __init__(self, interpreter: 'Interpreter') -> None: super().__init__(interpreter) + self._bindgen_bin: T.Optional['ExternalProgram'] = None @permittedKwargs(permitted_kwargs['test'] | {'dependencies'} ^ {'protocol'}) @typed_pos_args('rust.test', str, BuildTargetHolder) @@ -127,6 +130,77 @@ class RustModule(ExtensionModule): return ModuleReturnValue([], [e, test]) + @noPosargs + @permittedKwargs({'input', 'output', 'include_directories', 'c_args', 'args'}) + def bindgen(self, state: 'ModuleState', args: T.List, kwargs: T.Dict[str, T.Any]) -> ModuleReturnValue: + """Wrapper around bindgen to simplify it's use. + + The main thing this simplifies is the use of `include_directory` + objects, instead of having to pass a plethora of `-I` arguments. + """ + header: T.Union[File, CustomTarget, GeneratedList, CustomTargetIndex] + _deps: T.Sequence[T.Union[File, CustomTarget, GeneratedList, CustomTargetIndex]] + try: + header, *_deps = unholder(self.interpreter.source_strings_to_files(listify(kwargs['input']))) + except KeyError: + raise InvalidArguments('rustmod.bindgen() `input` argument must have at least one element.') + + try: + output: str = kwargs['output'] + except KeyError: + raise InvalidArguments('rustmod.bindgen() `output` must be provided') + if not isinstance(output, str): + raise InvalidArguments('rustmod.bindgen() `output` argument must be a string.') + + include_dirs: T.List[IncludeDirs] = typeslistify(unholder(listify(kwargs.get('include_directories', []))), IncludeDirs) + c_args: T.List[str] = stringlistify(listify(kwargs.get('c_args', []))) + bind_args: T.List[str] = stringlistify(listify(kwargs.get('args', []))) + + # Split File and Target dependencies to add pass to CustomTarget + depends: T.List[BuildTarget] = [] + depend_files: T.List[File] = [] + for d in _deps: + if isinstance(d, File): + depend_files.append(d) + else: + depends.append(d) + + inc_strs: T.List[str] = [] + for i in include_dirs: + # bindgen always uses clang, so it's safe to hardcode -I here + inc_strs.extend([f'-I{x}' for x in i.to_string_list(state.environment.get_source_dir())]) + + if self._bindgen_bin is None: + # there's some bugs in the interpreter typeing. + self._bindgen_bin = T.cast('ExternalProgram', self.interpreter.find_program_impl(['bindgen']).held_object) + + name: str + if isinstance(header, File): + name = header.fname + else: + name = header.get_outputs()[0] + + target = CustomTarget( + f'rustmod-bindgen-{name}'.replace('/', '_'), + state.subdir, + state.subproject, + { + 'input': header, + 'output': output, + 'command': self._bindgen_bin.get_command() + [ + '@INPUT@', '--output', + os.path.join(state.environment.build_dir, '@OUTPUT@')] + + bind_args + ['--'] + c_args + inc_strs + + ['-MD', '-MQ', '@INPUT@', '-MF', '@DEPFILE@'], + 'depfile': '@PLAINNAME@.d', + 'depends': depends, + 'depend_files': depend_files, + }, + backend=state.backend, + ) + + return ModuleReturnValue([target], [CustomTargetHolder(target, self.interpreter)]) + def initialize(*args: T.List, **kwargs: T.Dict) -> RustModule: return RustModule(*args, **kwargs) # type: ignore diff --git a/test cases/rust/12 bindgen/include/other.h b/test cases/rust/12 bindgen/include/other.h new file mode 100644 index 000000000..3f715f169 --- /dev/null +++ b/test cases/rust/12 bindgen/include/other.h @@ -0,0 +1,6 @@ +// SPDX-license-identifer: Apache-2.0 +// Copyright © 2021 Intel Corporation + +# pragma once + +#include diff --git a/test cases/rust/12 bindgen/meson.build b/test cases/rust/12 bindgen/meson.build new file mode 100644 index 000000000..7844884e2 --- /dev/null +++ b/test cases/rust/12 bindgen/meson.build @@ -0,0 +1,82 @@ +# SPDX-license-identifer: Apache-2.0 +# Copyright © 2021 Intel Corporation + +project('rustmod bindgen', ['c', 'rust']) + +prog_bindgen = find_program('bindgen', required : false) +if not prog_bindgen.found() + error('MESON_SKIP_TEST bindgen not found') +endif + +# This seems to happen on windows when libclang.dll is not in path or is not +# valid. We must try to process a header file for this to work. +# +# See https://github.com/rust-lang/rust-bindgen/issues/1797 +result = run_command(prog_bindgen, 'include/other.h') +if result.returncode() != 0 + error('MESON_SKIP_TEST bindgen does not seem to work') +endif + +# This is to test the include_directories argument to bindgen +inc = include_directories('include') + +c_lib = static_library('clib', 'src/source.c', include_directories : inc) + +rust = import('unstable-rust') + +gen = rust.bindgen( + input : 'src/header.h', + output : 'header.rs', + include_directories : inc, +) + +# see: https://github.com/mesonbuild/meson/issues/8160 +f = configure_file( + input : 'src/main.rs', + output : 'main.rs', + copy : true, +) + +rust_bin = executable( + 'rust_bin', + [f, gen], + link_with : c_lib, +) + +test('main', rust_bin) + +# Test a generated header +gen_h = custom_target( + 'gen.h', + command : [find_program('src/gen_header.py'), '@INPUT@', '@OUTPUT@'], + output : 'gen.h', + input : 'src/header.h' +) + +gen2_h = custom_target( + 'other.h', + command : [find_program('src/gen_header.py'), '@INPUT@', '@OUTPUT@'], + output : 'other.h', + input : 'include/other.h' +) + +gen_rs = rust.bindgen( + input : [gen_h, gen2_h], + output : 'gen.rs', +) + +f = configure_file( + input : 'src/main2.rs', + output : 'main2.rs', + copy : true, +) + +rust_bin2 = executable( + 'rust_bin2', + [f, gen_rs], + link_with : c_lib, +) + +test('generated header', rust_bin2) + +subdir('sub') diff --git a/test cases/rust/12 bindgen/src/gen_header.py b/test cases/rust/12 bindgen/src/gen_header.py new file mode 100644 index 000000000..099e4ec74 --- /dev/null +++ b/test cases/rust/12 bindgen/src/gen_header.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# SPDX-license-identifer: Apache-2.0 +# Copyright © 2021 Intel Corporation + +import argparse +import shutil + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('input') + parser.add_argument('output') + args = parser.parse_args() + + shutil.copy2(args.input, args.output) + + +if __name__ == "__main__": + main() diff --git a/test cases/rust/12 bindgen/src/header.h b/test cases/rust/12 bindgen/src/header.h new file mode 100644 index 000000000..2c03f3384 --- /dev/null +++ b/test cases/rust/12 bindgen/src/header.h @@ -0,0 +1,8 @@ +// SPDX-license-identifer: Apache-2.0 +// Copyright © 2021 Intel Corporation + +#pragma once + +#include "other.h" + +int32_t add(const int32_t, const int32_t); diff --git a/test cases/rust/12 bindgen/src/main.rs b/test cases/rust/12 bindgen/src/main.rs new file mode 100644 index 000000000..3f85e96be --- /dev/null +++ b/test cases/rust/12 bindgen/src/main.rs @@ -0,0 +1,14 @@ +// SPDX-license-identifer: Apache-2.0 +// Copyright © 2021 Intel Corporation + +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +include!("header.rs"); + +fn main() { + unsafe { + std::process::exit(add(0, 0)); + }; +} diff --git a/test cases/rust/12 bindgen/src/main2.rs b/test cases/rust/12 bindgen/src/main2.rs new file mode 100644 index 000000000..a3c28d78b --- /dev/null +++ b/test cases/rust/12 bindgen/src/main2.rs @@ -0,0 +1,15 @@ + +// SPDX-license-identifer: Apache-2.0 +// Copyright © 2021 Intel Corporation + +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +include!("gen.rs"); + +fn main() { + unsafe { + std::process::exit(add(0, 0)); + }; +} diff --git a/test cases/rust/12 bindgen/src/source.c b/test cases/rust/12 bindgen/src/source.c new file mode 100644 index 000000000..d652d28b9 --- /dev/null +++ b/test cases/rust/12 bindgen/src/source.c @@ -0,0 +1,8 @@ +// SPDX-license-identifer: Apache-2.0 +// Copyright © 2021 Intel Corporation + +#include "header.h" + +int32_t add(const int32_t first, const int32_t second) { + return first + second; +} diff --git a/test cases/rust/12 bindgen/sub/meson.build b/test cases/rust/12 bindgen/sub/meson.build new file mode 100644 index 000000000..1da360ebd --- /dev/null +++ b/test cases/rust/12 bindgen/sub/meson.build @@ -0,0 +1,39 @@ +gen_rs3 = rust.bindgen( + input : [gen_h, gen2_h], + output : 'gen.rs', +) + +f3 = configure_file( + input : '../src/main2.rs', + output : 'main3.rs', + copy : true, +) + +rust_bin3 = executable( + 'rust_bin3', + [f3, gen_rs3], + link_with : c_lib, +) + +test('generated header (subdir)', rust_bin3) + +gen4 = rust.bindgen( + input : '../src/header.h', + output : 'header.rs', + include_directories : inc, +) + +# see: https://github.com/mesonbuild/meson/issues/8160 +f4 = configure_file( + input : '../src/main.rs', + output : 'main.rs', + copy : true, +) + +rust_bin4 = executable( + 'rust_bin_subdir', + [f4, gen4], + link_with : c_lib, +) + +test('static header (subdir)', rust_bin4)