Optional C++ namespaces (#4759)

Co-authored-by: Michael Winkelmann <michael@winkelmann.site>
This commit is contained in:
Wilston Oreo 2024-03-06 19:43:11 +01:00 committed by GitHub
parent 43266f7f76
commit bef532b5fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 148 additions and 19 deletions

View file

@ -11,6 +11,19 @@ set_property(CACHE DEFAULT_SLINT_EMBED_RESOURCES PROPERTY STRINGS
# INITIALIZE_FROM_VARIABLE DEFAULT_SLINT_EMBED_RESOURCES) # INITIALIZE_FROM_VARIABLE DEFAULT_SLINT_EMBED_RESOURCES)
function(SLINT_TARGET_SOURCES target) function(SLINT_TARGET_SOURCES target)
# Parse the NAMESPACE argument
cmake_parse_arguments(SLINT_TARGET_SOURCES "" "NAMESPACE" "" ${ARGN})
if (DEFINED SLINT_TARGET_SOURCES_NAMESPACE)
# Remove the NAMESPACE argument from the list
list(FIND ARGN "NAMESPACE" _index)
list(REMOVE_AT ARGN ${_index})
list(FIND ARGN "${SLINT_TARGET_SOURCES_NAMESPACE}" _index)
list(REMOVE_AT ARGN ${_index})
# If the namespace is not empty, add the --cpp-namespace argument
set(_SLINT_CPP_NAMESPACE_ARG "--cpp-namespace=${SLINT_TARGET_SOURCES_NAMESPACE}")
endif()
foreach (it IN ITEMS ${ARGN}) foreach (it IN ITEMS ${ARGN})
get_filename_component(_SLINT_BASE_NAME ${it} NAME_WE) get_filename_component(_SLINT_BASE_NAME ${it} NAME_WE)
get_filename_component(_SLINT_ABSOLUTE ${it} REALPATH BASE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) get_filename_component(_SLINT_ABSOLUTE ${it} REALPATH BASE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
@ -28,6 +41,7 @@ function(SLINT_TARGET_SOURCES target)
--style ${_SLINT_STYLE} --style ${_SLINT_STYLE}
--embed-resources=${embed} --embed-resources=${embed}
--translation-domain="${target}" --translation-domain="${target}"
${_SLINT_CPP_NAMESPACE_ARG}
DEPENDS Slint::slint-compiler ${_SLINT_ABSOLUTE} DEPENDS Slint::slint-compiler ${_SLINT_ABSOLUTE}
COMMENT "Generating ${_SLINT_BASE_NAME}.h" COMMENT "Generating ${_SLINT_BASE_NAME}.h"
DEPFILE ${CMAKE_CURRENT_BINARY_DIR}/${_SLINT_BASE_NAME}.d DEPFILE ${CMAKE_CURRENT_BINARY_DIR}/${_SLINT_BASE_NAME}.d

View file

@ -5,23 +5,26 @@
## `slint_target_sources` ## `slint_target_sources`
``` ```
slint_target_sources(<target> <files>....) slint_target_sources(<target> <files>.... [NAMESPACE namespace])
``` ```
Use this function to tell cmake about the .slint files of your application, similar to the builtin cmake [target_sources](https://cmake.org/cmake/help/latest/command/target_sources.html) function. Use this function to tell cmake about the .slint files of your application, similar to the builtin cmake [target_sources](https://cmake.org/cmake/help/latest/command/target_sources.html) function.
The function takes care of running the slint-compiler to convert `.slint` files to `.h` files in the build directory, The function takes care of running the slint-compiler to convert `.slint` files to `.h` files in the build directory,
and extend the include directories of your target so that the generated file is found when including it in your application. and extend the include directories of your target so that the generated file is found when including it in your application.
The optional NAMESPACE argument will put the generated components in the given C++ namespace.
Given a file called `the_window.slint`, the following example will create a file called `the_window.h` that can Given a file called `the_window.slint`, the following example will create a file called `the_window.h` that can
be included from your .cpp file. be included from your .cpp file. Assuming the `the_window.slint` contains a component `TheWindow`, the output
C++ class will be put in the namespace `ui`, resulting to `ui::TheWindow`.
```cmake ```cmake
add_executable(my_application main.cpp) add_executable(my_application main.cpp)
target_link_libraries(my_application PRIVATE Slint::Slint) target_link_libraries(my_application PRIVATE Slint::Slint)
slint_target_sources(my_application the_window.slint) slint_target_sources(my_application the_window.slint NAMESPACE ui)
``` ```
## Resource Embedding ## Resource Embedding
By default, images from [`@image-url()`](slint-reference:src/language/syntax/types#images) or fonts that your Slint files reference are loaded from disk at run-time. This minimises build times, but requires that the directory structure with the files remains stable. If you want to build a program that runs anywhere, then you can configure the Slint compiler to embed such sources into the binary. By default, images from [`@image-url()`](slint-reference:src/language/syntax/types#images) or fonts that your Slint files reference are loaded from disk at run-time. This minimises build times, but requires that the directory structure with the files remains stable. If you want to build a program that runs anywhere, then you can configure the Slint compiler to embed such sources into the binary.

View file

@ -10,4 +10,4 @@ endif()
add_executable(todo main.cpp) add_executable(todo main.cpp)
target_link_libraries(todo PRIVATE Slint::Slint) target_link_libraries(todo PRIVATE Slint::Slint)
slint_target_sources(todo ../ui/todo.slint) slint_target_sources(todo ../ui/todo.slint NAMESPACE todo_ui)

View file

@ -5,7 +5,8 @@
int main() int main()
{ {
auto demo = MainWindow::create(); auto demo = todo_ui::MainWindow::create();
using todo_ui::TodoItem;
auto todo_model = std::make_shared<slint::VectorModel<TodoItem>>(std::vector { auto todo_model = std::make_shared<slint::VectorModel<TodoItem>>(std::vector {
TodoItem { "Implement the .slint file", true }, TodoItem { "Implement the .slint file", true },

View file

@ -18,15 +18,15 @@ use crate::namedreference::NamedReference;
use crate::object_tree::{Component, Document, ElementRc}; use crate::object_tree::{Component, Document, ElementRc};
#[cfg(feature = "cpp")] #[cfg(feature = "cpp")]
mod cpp; pub mod cpp;
#[cfg(feature = "rust")] #[cfg(feature = "rust")]
pub mod rust; pub mod rust;
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum OutputFormat { pub enum OutputFormat {
#[cfg(feature = "cpp")] #[cfg(feature = "cpp")]
Cpp, Cpp(cpp::Config),
#[cfg(feature = "rust")] #[cfg(feature = "rust")]
Rust, Rust,
Interpreter, Interpreter,
@ -37,7 +37,9 @@ impl OutputFormat {
pub fn guess_from_extension(path: &std::path::Path) -> Option<Self> { pub fn guess_from_extension(path: &std::path::Path) -> Option<Self> {
match path.extension().and_then(|ext| ext.to_str()) { match path.extension().and_then(|ext| ext.to_str()) {
#[cfg(feature = "cpp")] #[cfg(feature = "cpp")]
Some("cpp") | Some("cxx") | Some("h") | Some("hpp") => Some(Self::Cpp), Some("cpp") | Some("cxx") | Some("h") | Some("hpp") => {
Some(Self::Cpp(cpp::Config::default()))
}
#[cfg(feature = "rust")] #[cfg(feature = "rust")]
Some("rs") => Some(Self::Rust), Some("rs") => Some(Self::Rust),
_ => None, _ => None,
@ -50,7 +52,7 @@ impl std::str::FromStr for OutputFormat {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
#[cfg(feature = "cpp")] #[cfg(feature = "cpp")]
"cpp" => Ok(Self::Cpp), "cpp" => Ok(Self::Cpp(cpp::Config::default())),
#[cfg(feature = "rust")] #[cfg(feature = "rust")]
"rust" => Ok(Self::Rust), "rust" => Ok(Self::Rust),
"llr" => Ok(Self::Llr), "llr" => Ok(Self::Llr),
@ -74,8 +76,8 @@ pub fn generate(
match format { match format {
#[cfg(feature = "cpp")] #[cfg(feature = "cpp")]
OutputFormat::Cpp => { OutputFormat::Cpp(config) => {
let output = cpp::generate(doc); let output = cpp::generate(doc, config);
write!(destination, "{}", output)?; write!(destination, "{}", output)?;
} }
#[cfg(feature = "rust")] #[cfg(feature = "rust")]

View file

@ -8,6 +8,12 @@
use std::fmt::Write; use std::fmt::Write;
/// The configuration for the C++ code generator
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Config {
pub namespace: Option<String>,
}
fn ident(ident: &str) -> String { fn ident(ident: &str) -> String {
if ident.contains('-') { if ident.contains('-') {
ident.replace('-', "_") ident.replace('-', "_")
@ -76,6 +82,7 @@ mod cpp_ast {
pub struct File { pub struct File {
pub includes: Vec<String>, pub includes: Vec<String>,
pub after_includes: String, pub after_includes: String,
pub namespace: Option<String>,
pub declarations: Vec<Declaration>, pub declarations: Vec<Declaration>,
pub definitions: Vec<Declaration>, pub definitions: Vec<Declaration>,
} }
@ -85,6 +92,11 @@ mod cpp_ast {
for i in &self.includes { for i in &self.includes {
writeln!(f, "#include {}", i)?; writeln!(f, "#include {}", i)?;
} }
if let Some(namespace) = &self.namespace {
writeln!(f, "namespace {} {{", namespace)?;
INDENTATION.with(|x| x.set(x.get() + 1));
}
write!(f, "{}", self.after_includes)?; write!(f, "{}", self.after_includes)?;
for d in &self.declarations { for d in &self.declarations {
write!(f, "\n{}", d)?; write!(f, "\n{}", d)?;
@ -92,6 +104,11 @@ mod cpp_ast {
for d in &self.definitions { for d in &self.definitions {
write!(f, "\n{}", d)?; write!(f, "\n{}", d)?;
} }
if let Some(namespace) = &self.namespace {
writeln!(f, "}} // namespace {}", namespace)?;
INDENTATION.with(|x| x.set(x.get() - 1));
}
Ok(()) Ok(())
} }
} }
@ -515,8 +532,8 @@ fn handle_property_init(
} }
/// Returns the text of the C++ code produced by the given root component /// Returns the text of the C++ code produced by the given root component
pub fn generate(doc: &Document) -> impl std::fmt::Display { pub fn generate(doc: &Document, config: Config) -> impl std::fmt::Display {
let mut file = File::default(); let mut file = File { namespace: config.namespace.clone(), ..Default::default() };
file.includes.push("<array>".into()); file.includes.push("<array>".into());
file.includes.push("<limits>".into()); file.includes.push("<limits>".into());

View file

@ -94,6 +94,9 @@ pub struct CompilerConfiguration {
/// The domain used as one of the parameter to the translate function /// The domain used as one of the parameter to the translate function
pub translation_domain: Option<String>, pub translation_domain: Option<String>,
/// C++ namespace
pub cpp_namespace: Option<String>,
} }
impl CompilerConfiguration { impl CompilerConfiguration {
@ -142,6 +145,18 @@ impl CompilerConfiguration {
let enable_component_containers = enable_experimental_features; let enable_component_containers = enable_experimental_features;
let cpp_namespace = match output_format {
#[cfg(feature = "cpp")]
crate::generator::OutputFormat::Cpp(config) => match config.namespace {
Some(namespace) => Some(namespace),
None => match std::env::var("SLINT_CPP_NAMESPACE") {
Ok(namespace) => Some(namespace),
Err(_) => None,
},
},
_ => None,
};
Self { Self {
embed_resources, embed_resources,
include_paths: Default::default(), include_paths: Default::default(),
@ -154,6 +169,7 @@ impl CompilerConfiguration {
accessibility: true, accessibility: true,
enable_component_containers, enable_component_containers,
translation_domain: None, translation_domain: None,
cpp_namespace,
} }
} }
} }

View file

@ -0,0 +1,36 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
//cpp-namespace: my::ui
//ignore: rust,js
struct TestStruct {
condition: bool,
}
enum TestEnum {
A,
B,
C
}
export component TestCase inherits Rectangle {
in-out property <TestStruct> test_struct;
in-out property <TestEnum> test_enum;
}
/*
```cpp
auto handle = my::ui::TestCase::create();
const my::ui::TestCase &instance = *handle;
my::ui::TestStruct test_struct {.condition = false};
test_struct.condition = true;
instance.set_test_struct(test_struct);
assert(instance.get_test_struct() == test_struct);
instance.set_test_enum(my::ui::TestEnum::A);
auto test_enum = instance.get_test_enum();
test_enum = my::ui::TestEnum::B;
instance.set_test_enum(test_enum);
assert(instance.get_test_enum() == my::ui::TestEnum::B);
```
*/

View file

@ -16,9 +16,14 @@ pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>>
.map(|(k, v)| (k.to_string(), std::path::PathBuf::from(v))) .map(|(k, v)| (k.to_string(), std::path::PathBuf::from(v)))
.collect::<std::collections::HashMap<_, _>>(); .collect::<std::collections::HashMap<_, _>>();
let cpp_namespace = test_driver_lib::extract_cpp_namespace(&source);
let mut diag = BuildDiagnostics::default(); let mut diag = BuildDiagnostics::default();
let syntax_node = parser::parse(source.clone(), Some(&testcase.absolute_path), None, &mut diag); let syntax_node = parser::parse(source.clone(), Some(&testcase.absolute_path), None, &mut diag);
let mut compiler_config = CompilerConfiguration::new(generator::OutputFormat::Cpp); let output_format =
generator::OutputFormat::Cpp(generator::cpp::Config { namespace: cpp_namespace });
let mut compiler_config = CompilerConfiguration::new(output_format.clone());
compiler_config.include_paths = include_paths; compiler_config.include_paths = include_paths;
compiler_config.library_paths = library_paths; compiler_config.library_paths = library_paths;
let (root_component, diag, _) = let (root_component, diag, _) =
@ -31,7 +36,7 @@ pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>>
let mut generated_cpp: Vec<u8> = Vec::new(); let mut generated_cpp: Vec<u8> = Vec::new();
generator::generate(generator::OutputFormat::Cpp, &mut generated_cpp, &root_component)?; generator::generate(output_format, &mut generated_cpp, &root_component)?;
if diag.has_error() { if diag.has_error() {
let vec = diag.to_string_vec(); let vec = diag.to_string_vec();

View file

@ -238,3 +238,23 @@ fn test_extract_ignores() {
let r = extract_ignores(source).collect::<Vec<_>>(); let r = extract_ignores(source).collect::<Vec<_>>();
assert_eq!(r, ["cpp", "rust", "nodejs"]); assert_eq!(r, ["cpp", "rust", "nodejs"]);
} }
pub fn extract_cpp_namespace(source: &str) -> Option<String> {
lazy_static::lazy_static! {
static ref RX: Regex = Regex::new(r"//cpp-namespace:\s*(.+)\s*\n").unwrap();
}
RX.captures(source).map(|mat| mat.get(1).unwrap().as_str().trim().to_string())
}
#[test]
fn test_extract_cpp_namespace() {
assert!(extract_cpp_namespace("something").is_none());
let source = r"
//cpp-namespace: ui
Blah {}
";
let r = extract_cpp_namespace(source);
assert_eq!(r, Some("ui".to_string()));
}

View file

@ -62,6 +62,10 @@ struct Cli {
/// Translation domain /// Translation domain
#[arg(long = "translation-domain", action)] #[arg(long = "translation-domain", action)]
translation_domain: Option<String>, translation_domain: Option<String>,
/// C++ namespace
#[arg(long = "cpp-namespace", name = "C++ namespace")]
cpp_namespace: Option<String>,
} }
fn main() -> std::io::Result<()> { fn main() -> std::io::Result<()> {
@ -74,7 +78,18 @@ fn main() -> std::io::Result<()> {
diag.print(); diag.print();
std::process::exit(-1); std::process::exit(-1);
} }
let mut compiler_config = CompilerConfiguration::new(args.format);
let mut format = args.format.clone();
if args.cpp_namespace.is_some() {
if !matches!(format, generator::OutputFormat::Cpp(..)) {
eprintln!("C++ namespace option was set. Output format will be C++.");
}
format =
generator::OutputFormat::Cpp(generator::cpp::Config { namespace: args.cpp_namespace });
}
let mut compiler_config = CompilerConfiguration::new(format.clone());
compiler_config.translation_domain = args.translation_domain; compiler_config.translation_domain = args.translation_domain;
// Override defaults from command line: // Override defaults from command line:
@ -102,10 +117,10 @@ fn main() -> std::io::Result<()> {
let diag = diag.check_and_exit_on_error(); let diag = diag.check_and_exit_on_error();
if args.output == std::path::Path::new("-") { if args.output == std::path::Path::new("-") {
generator::generate(args.format, &mut std::io::stdout(), &doc)?; generator::generate(format, &mut std::io::stdout(), &doc)?;
} else { } else {
generator::generate( generator::generate(
args.format, format,
&mut BufWriter::new(std::fs::File::create(&args.output)?), &mut BufWriter::new(std::fs::File::create(&args.output)?),
&doc, &doc,
)?; )?;