Compare commits

...
9 Commits
13 changed files with 667 additions and 8 deletions
+14 -1
View File
@@ -2,4 +2,17 @@
## Bump Version Up
TODO
### Bump OMRF Version Up
- Change the version declared in `Cargo.toml`.
### Bump OMRF Packer Version Up
- Change the version declared in `pyproject.toml`.
- Change the version constant `VERSION` declared in `utils.py`.
- Change the minimum required version declared in the metadata example section of `README.md`.
## Version Tag
- Use `omrf/x.y.z` to tag the version of `sarasacw-omrf`.
- Use `omrf-packer/x.y.z` to tag the version of `sarasacw-omrf-packer`.
@@ -0,0 +1,50 @@
# Allowed Data Types
Under this section we discuss which data types are allowed to appear in an FFI
interface and how to convert Rust data types into these allowed types. The
correspondence between these types and their C/C++ counterparts is covered in
[C/C++ Headers](c-cpp-headers.md).
## Primitive Type Conversion
This subsection covers the conversion of primitive types. The rules described here
apply to both parameter and return value positions.
### Types Allowed to Cross the Boundary Directly
The following Rust types are allowed to be passed directly across the FFI boundary:
- `bool`
- `f32`
- `f64`
- `i8`
- `i16`
- `i32`
- `i64`
- `u8`
- `u16`
- `u32`
- `u64`
- `usize`
- `isize`
### The u128 and i128 Problem
The `u128` and `i128` types have very poor support in the C/C++ standard libraries.
Therefore they cannot be passed directly across the FFI boundary.
To transfer data of these types, refer to the later section about passing opaque
structs.
### The char Problem
The Rust `char` type only holds valid Unicode scalar values. The Rust official
documentation explicitly states that the `char` type does not have FFI safety, so it
cannot be used as a value passed across the FFI boundary.
The solution is to use the Rust `u32` type for passing. Under this scheme:
- The function signature uses `u32` as the parameter type.
- For input parameters, use `char::try_from` to perform a fallible conversion that
validates the incoming character.
- For output parameters, use `u32::from` for the conversion.
@@ -0,0 +1,85 @@
# C/C++ Headers
Under this section we discuss how to write the C/C++ header files that accompany the
Rust FFI library. The Rust-side counterpart of each topic is covered in
[FFI Function Signatures](ffi-function-signatures.md) and
[Allowed Data Types](allowed-data-types.md).
## Function Declaration Style
FFI functions must be exported under C names, that is, they must not go through the
C++ name mangling mechanism. In the generated C header file, you need to use the
`extern "C"` specifier, the `__cplusplus` macro and macro conditionals to import the
declarations correctly, for example:
```c++
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
CError WFStartup(void);
// More FFI functions omitted...
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
```
## Parameter Declarations
A declaration of an exported FFI function in the C header file generally looks like
the following:
```c++
CError FBAdd(
OMRF_IN_PARAM_TY(uint32_t, in_a),
OMRF_IN_PARAM_TY(uint32_t, in_b),
OMRF_OUT_PARAM_TY(uint32_t, out_rv),
OMRF_OUT_PARAM_TY(bool, out_overflow));
```
## Required Header Files
The generated C/C++ header files must include the appropriate header files for the
types used in the declarations.
- In C, the `bool` type is provided by `<stddef.h>`. C++ supports `bool` natively
and does not need any header file.
- In C, the integer types are provided by `<stdint.h>`. In C++, they are provided
by `<cstdint>`.
- In C, the `uintptr_t` and `intptr_t` types are provided by `<stdint.h>`. In C++,
they are provided by `<cstdint>`.
- The floating-point types do not require any header file.
## Type Correspondences
The following table shows the correspondence between the Rust types that can cross
the FFI boundary directly (listed in [Allowed Data Types](allowed-data-types.md))
and their C/C++ counterparts. The left column lists the Rust type and the right
column lists the corresponding C/C++ type.
| Rust | C/C++ |
|------|-------|
| `bool` | `bool` |
| `f32` | `float` |
| `f64` | `double` |
| `i8` | `int8_t` |
| `i16` | `int16_t` |
| `i32` | `int32_t` |
| `i64` | `int64_t` |
| `u8` | `uint8_t` |
| `u16` | `uint16_t` |
| `u32` | `uint32_t` |
| `u64` | `uint64_t` |
| `usize` | `uintptr_t` |
| `isize` | `intptr_t` |
## The char Problem
For the Rust `char` type, the C/C++ header side uses `char32_t` as the parameter
type. Note that `char32_t` can hold any 32-bit unsigned integer, which is broader
than the set of valid Unicode scalar values carried by the Rust `char` type. The
Rust side is responsible for validating the incoming value; see
[The char Problem](allowed-data-types.md#the-char-problem) in
[Allowed Data Types](allowed-data-types.md).
@@ -0,0 +1,169 @@
# FFI Function Signatures
Under this section we discuss the signature requirements of the exported FFI
functions.
## C-style Name Export
FFI functions must be exported under C names, that is, they must not go through the
C++ name mangling mechanism.
In Rust, you need to decorate the function with the `#[unsafe(no_mangle)]` attribute
together with `extern "C"`, for example:
```rust
#[unsafe(no_mangle)]
pub extern "C" fn WFStartup() -> CError {
// Function body omitted...
}
// More FFI functions omitted...
```
## Export Name Style
In this subsection, we discuss the naming style of exported functions.
In this design, there are exactly two styles to choose from. We call them the
Windows style and the POSIX style. Either style can be chosen freely, but within a
single library the two styles must not be mixed.
We recommend the following selection rule: if your library is Windows-only, choose
the Windows style; otherwise, choose the POSIX style.
### Windows Style
The Windows style format is `<PREFIX><FUNC>` for ordinary functions, and
`<PREFIX><STRUCT><FUNC>` for opaque struct functions.
In the format, `<STRUCT>` is the name of the opaque struct and `<FUNC>` is the name
of the function. Both of them must use PascalCase naming.
For `<PREFIX>`, it is a prefix identifier that is specific to this library. As we
all know, C has no namespace concept, and all functions are exported into the same
space. Therefore, avoiding name collisions between functions is an important task.
By prepending `<PREFIX>` to the function name, name duplication can be avoided as
much as possible. For this reason, you need to choose `<PREFIX>` carefully and try
to pick a prefix that is unlikely to collide.
In the Windows style, there are two spellings for `<PREFIX>`. One is the
all-uppercase style and the other is the style in which only the first letter is
uppercase and all remaining letters are lowercase. Choose according to your own
needs. In addition, the length of `<PREFIX>` must not exceed 5 characters, and
it must not contain anything other than letters and digits (that is, underscores are
not allowed).
The following examples show some legal and illegal Windows style function names:
| Name | Verdict | Reason |
|------|---------|--------|
| `WFStartup` | Legal | An ordinary (non opaque struct) function. |
| `WFIconGetHandle` | Legal | An opaque struct function whose `<STRUCT>` is `Icon`. |
| `WFWordHandleGetHandle` | Legal | An opaque struct function whose `<STRUCT>` is `WordHandle`. |
| `FlGetMessage` | Legal | Uses a `<PREFIX>`, which is `Fl`, that capitalizes only its first letter. |
| `MyRustGetMessage` | Illegal | `<PREFIX>`, which is `MyRust`, does not keep all remaining letters lowercase. |
| `My_RustGetMessage` | Illegal | `<PREFIX>` contains an underscore. |
| `IconGetHandle` | Illegal | Missing `<PREFIX>`. |
| `QwertyuiopGetData` | Illegal | `<PREFIX>`, which is `Qwertyuiop`, is too long. |
| `WFget_message` | Illegal | `<FUNC>`, which is `get_message`, is not PascalCase. |
| `WFicon_handleGetMessage` | Illegal | `<STRUCT>`, which is `icon_handle`, is not PascalCase. |
### POSIX Style
The POSIX style format is `<PREFIX>_<FUNC>` for ordinary functions, and
`<PREFIX>_<STRUCT>_<FUNC>` for opaque struct functions.
The `<PREFIX>`, `<STRUCT>` and `<FUNC>` components in the format have the same
meaning as in the Windows style, but the format is different. `<STRUCT>` and `<FUNC>`
must use snake_case naming. `<PREFIX>` must be all lowercase, and it must not contain
anything other than letters and digits (that is, underscores are not allowed). Its
length and selection requirements are the same as in the Windows style.
The following examples show some legal and illegal POSIX style function names:
| Name | Verdict | Reason |
|------|---------|--------|
| `wf_startup` | Legal | An ordinary (non opaque struct) function. |
| `wf_icon_get_handle` | Legal | An opaque struct function whose `<STRUCT>` is `icon`. |
| `wf_word_handle_get_handle` | Legal | An opaque struct function whose `<STRUCT>` is `word_handle`. |
| `Fl_get_message` | Illegal | `<PREFIX>`, which is `Fl`, is not all lowercase. |
| `my_rust_get_message` | Illegal | `<PREFIX>`, which is `my_rust`, contains an underscore. |
| `icon_get_handle` | Illegal | Missing `<PREFIX>`. |
| `qwertyuiop_get_data` | Illegal | `<PREFIX>`, which is `qwertyuiop`, is too long. |
| `wf_GetMessage` | Illegal | `<FUNC>`, which is `GetMessage`, is not snake_case. |
| `wf_Icon_get_message` | Illegal | `<STRUCT>`, which is `Icon`, is not snake_case. |
## Parameters and Return Value
Under this section we discuss the parameters and return value of the exported
functions. A standard FFI function looks like the following on the Rust side:
```rust
#[unsafe(no_mangle)]
pub extern "C" fn FBAdd(
in_a: in_param_ty!(u32),
in_b: in_param_ty!(u32),
out_rv: out_param_ty!(u32),
out_overflow: out_param_ty!(bool),
) -> CError {
// Function body omitted...
}
```
As shown in the example, this function has four parameters. The first two are input
parameters and the last two are output parameters. The return value of the function
is `CError`.
### Parameter Rules
To follow the best practice of C for the signature of functions with any number of
input parameters and output parameters, we must design the function signature this
way. To implement such a function that accepts any number of inputs and outputs, we
need to put both the input parameters and the output parameters into the parameter
list. All input parameters must precede any output parameter. The return value,
`CError`, is used only to indicate whether the function succeeded or failed.
The naming style of the function parameters (both input and output) must be
snake_case and must not start with an underscore.
#### Input Parameters
Input parameters are passed directly by value (for example, integers, floats or
pointers). In the example, `in_a` and `in_b` are both of type `u32`. You can use
the `in_param_ty!` macro provided by the crate to conveniently mark this.
By convention, input parameters are prefixed with `in_`, but this is not enforced.
#### Output Parameters
Output parameters are passed as pointers. In the example, `out_rv` is actually of
type `*mut u32` and `out_overflow` is actually of type `*mut bool`. To avoid
repeatedly writing these pointer types and to avoid writing them incorrectly, you
can use the `out_param_ty!` macro provided by the crate, which is symmetric with
`in_param_ty!`.
At the same time, you can use the `deref_out_param!` macro provided by the crate to
conveniently dereference an output parameter for assignment, or directly use the
`set_out_param!` macro provided by the crate for assignment. But you usually do not
need to do that, because in later chapters you will be guided on how to correctly
use `cffi_wrapper!` to maximize the efficiency of writing FFI functions.
By convention, output parameters are prefixed with `out_`, but this is not enforced.
### The Return Value
The return value of an FFI function must be of the `CError` type.
`CError` is defined in `last_error::CError` as a plain `u32`. It is not an enum with
a fixed set of members. Instead, `CERROR_OK` is the single value that represents
absolute, unambiguous success. Every other value's meaning is decided by the
consuming project: it may denote a failure, or, like some Win32 functions, a
partial success.
#### Migrating from a bool Return Value
Projects that previously returned a plain `bool` can adopt the error-code scheme
with minimal change: define exactly one non-success code, for example
`const CERROR_FAIL: CError = 1;`, and map every error type to it. The result is
equivalent to a `bool`: `CERROR_OK` for success and `CERROR_FAIL` for any failure,
while leaving room to introduce finer-grained codes later.
+13
View File
@@ -0,0 +1,13 @@
# C-friendly FFI Design
This document is a guide for authoring Rust libraries that expose a C-friendly FFI
and distributing them as ordinary CMake/pkg-config packages. It covers how to design
C-friendly FFI signatures, how to pass primitive types, enums, strings and
Rust-specific constructs such as `Option` and `Result` across the boundary, and how
to write the accompanying C/C++ header files.
## Contents
- [FFI Function Signatures](ffi-function-signatures.md)
- [Allowed Data Types](allowed-data-types.md)
- [C/C++ Headers](c-cpp-headers.md)
View File
View File
+1
View File
@@ -1,6 +1,7 @@
[package]
name = "sarasacw-omrf"
version = "1.0.0"
authors = ["yyc12345 <yyc12321@outlook.com>"]
edition = "2024"
rust-version = "1.85"
description = "The facilities for Rust, specifically designed for the SarasaCW OMRF project, to support C-friendly FFI."
+1
View File
@@ -1,6 +1,7 @@
pub mod cffi;
pub mod cstr_ffi;
pub mod last_error;
pub mod library_lifecycle;
pub mod object_pool;
#[macro_export]
+155
View File
@@ -0,0 +1,155 @@
//! Reference-counted startup/shutdown lifecycle for a DLL-style FFI library, in the spirit of
//! COM's `CoInitialize` / `CoUninitialize`.
//!
//! [`LibraryLifecycle`] holds an internal reference count together with a piece of state `S`:
//!
//! - on the `0 -> 1` transition ([`startup`](LibraryLifecycle::startup)) the supplied init closure
//! runs and its produced `S` is stored;
//! - on the `-> 0` transition ([`shutdown`](LibraryLifecycle::shutdown)) the stored `S` is taken
//! out and handed to the supplied destroy closure.
//!
//! Repeated calls are allowed as long as they are paired. The count and the state are guarded by a
//! [`RwLock`]; [`with_state`](LibraryLifecycle::with_state) takes a read lock (read access proceeds
//! concurrently) while `startup` / `shutdown` take a write lock.
//!
//! # Typical usage
//!
//! A single instance is usually stored in a `static LazyLock` and shared across all FFI entry
//! points. The state `S` typically aggregates the library's DLL-level resources -- for example a
//! number of [`ObjectPool`](crate::object_pool::ObjectPool)s and other globals that must be
//! constructed on first startup and torn down on last shutdown.
//!
//! # Call order
//!
//! Every `shutdown` must match a preceding successful `startup`. Calling `shutdown` when the count
//! is already zero is an error.
//!
//! # Closures
//!
//! Init and destroy closures are supplied **per call** (not at construction). Only the closure
//! passed to the transition call (`0 -> 1` for init, `-> 0` for destroy) is actually run; on any
//! other call the passed closure is dropped without being run, so callers should pass the same
//! init/destroy each time.
//!
//! The init closure may fail; its error is type-erased. The destroy closure must never fail
//! (destructor semantics) -- if its cleanup can fail it must be handled inside the closure.
//!
//! # Re-entrancy
//!
//! Closures run while the internal lock is held and **must not re-enter** the same
//! [`LibraryLifecycle`] (the lock is not re-entrant and would deadlock).
use std::sync::RwLock;
use thiserror::Error as TeError;
struct Inner<S> {
count: usize,
state: Option<S>,
}
/// Reference-counted startup/shutdown lifecycle holding a piece of state `S`.
///
/// See the module-level documentation for the overall contract, call order, and typical usage.
pub struct LibraryLifecycle<S> {
inner: RwLock<Inner<S>>,
}
/// Error returned by [`LibraryLifecycle`] operations.
#[derive(Debug, TeError)]
pub enum Error {
/// The initialization closure failed; the original error is type-erased.
#[error("initialization failed: {0}")]
Init(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
/// `shutdown` was called when the reference count was already zero (unbalanced call).
#[error("shutdown called when reference count is already zero")]
Underflow,
/// The state was accessed while the lifecycle is not active (before `startup` or after the
/// matching `shutdown`).
#[error("state accessed while not active")]
NotActive,
}
impl<S> LibraryLifecycle<S> {
/// Create a new lifecycle with reference count zero and no state.
pub fn new() -> Self {
Self {
inner: RwLock::new(Inner {
count: 0,
state: None,
}),
}
}
/// Increment the reference count.
///
/// On the `0 -> 1` transition the supplied `init` closure runs and its produced `S` is stored;
/// the call returns `Ok(true)`. If `init` fails, neither the count nor the state is changed and
/// an error is returned.
///
/// On any other call (count already `> 0`) the supplied `init` is **dropped without being run**,
/// the count is simply incremented, and the call returns `Ok(false)`.
///
/// The init closure runs under the write lock and must not re-enter this lifecycle. Its error
/// type `E` is type-erased.
pub fn startup<E>(&self, init: impl FnOnce() -> Result<S, E>) -> Result<bool, Error>
where
E: std::error::Error + Send + Sync + 'static,
{
let mut inner = self.inner.write().expect("unexpected poison lock");
if inner.count == 0 {
let s = init().map_err(|e| Error::Init(Box::new(e)))?;
inner.state = Some(s);
inner.count = 1;
Ok(true)
} else {
inner.count += 1;
Ok(false)
}
}
/// Decrement the reference count.
///
/// On the `-> 0` transition the stored `S` is taken out and handed to the supplied `destroy`
/// closure, which must never fail (destructor semantics), and the call returns `Ok(true)`. On
/// any other call (count stays `> 0`) the supplied `destroy` is **dropped without being run**,
/// the count is simply decremented, and the call returns `Ok(false)`.
///
/// Returns an error if the count was already zero.
///
/// The destroy closure runs under the write lock and must not re-enter this lifecycle.
pub fn shutdown(&self, destroy: impl FnOnce(S)) -> Result<bool, Error> {
let mut inner = self.inner.write().expect("unexpected poison lock");
if inner.count == 0 {
return Err(Error::Underflow);
}
inner.count -= 1;
if inner.count == 0 {
// Invariant: state is Some whenever count > 0.
let s = inner.state.take().expect("state present while count > 0");
destroy(s);
Ok(true)
} else {
Ok(false)
}
}
/// Access the stored state by shared reference under a read lock.
///
/// Multiple `with_state` calls can run concurrently. Returns an error if the lifecycle is not
/// currently active (count is zero).
///
/// `S` is immutable from the outside; if mutation is needed, build it into `S` via interior
/// mutability (e.g. `Mutex`, atomics). The closure must not re-enter this lifecycle.
pub fn with_state<R>(&self, f: impl FnOnce(&S) -> R) -> Result<R, Error> {
let inner = self.inner.read().expect("unexpected poison lock");
match &inner.state {
Some(s) => Ok(f(s)),
None => Err(Error::NotActive),
}
}
/// Current reference count.
pub fn count(&self) -> usize {
self.inner.read().expect("unexpected poison lock").count
}
}
+155
View File
@@ -0,0 +1,155 @@
//! Integration tests for the `library_lifecycle` module.
use sarasacw_omrf::library_lifecycle::LibraryLifecycle;
use std::sync::atomic::{AtomicUsize, Ordering};
use thiserror::Error as TeError;
#[derive(Debug, TeError)]
enum TestErr {
#[error("boom")]
Boom,
}
#[derive(Debug)]
struct State {
value: usize,
}
/// Coverage: a single paired startup/shutdown runs init once and destroy once, destroy receives the
/// state produced by init, both report that they ran the closure, and the count returns to zero.
#[test]
fn paired_startup_shutdown() {
let inits = AtomicUsize::new(0);
let destroys = AtomicUsize::new(0);
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
let ran_init = lc
.startup(|| -> Result<State, TestErr> {
inits.fetch_add(1, Ordering::SeqCst);
Ok(State { value: 42 })
})
.unwrap();
assert!(ran_init);
let ran_destroy = lc
.shutdown(|s| {
destroys.fetch_add(1, Ordering::SeqCst);
assert_eq!(s.value, 42);
})
.unwrap();
assert!(ran_destroy);
assert_eq!(inits.load(Ordering::SeqCst), 1);
assert_eq!(destroys.load(Ordering::SeqCst), 1);
assert_eq!(lc.count(), 0);
}
/// Coverage: multiple startups followed by matching shutdowns run init and destroy only on the
/// boundary transitions (once each); the init/destroy closures passed on non-transition calls are
/// dropped without being run, and the bool return reflects exactly which calls ran a closure.
#[test]
fn multiple_startups_shutdowns_run_only_on_boundaries() {
let inits = AtomicUsize::new(0);
let destroys = AtomicUsize::new(0);
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
let startup_ran: Vec<bool> = (0..3)
.map(|_| {
lc.startup(|| -> Result<State, TestErr> {
inits.fetch_add(1, Ordering::SeqCst);
Ok(State { value: 7 })
})
.unwrap()
})
.collect();
assert_eq!(startup_ran, vec![true, false, false]);
assert_eq!(lc.count(), 3);
assert_eq!(inits.load(Ordering::SeqCst), 1);
let shutdown_ran: Vec<bool> = (0..3)
.map(|_| {
lc.shutdown(|_s| {
destroys.fetch_add(1, Ordering::SeqCst);
})
.unwrap()
})
.collect();
assert_eq!(shutdown_ran, vec![false, false, true]);
assert_eq!(inits.load(Ordering::SeqCst), 1);
assert_eq!(destroys.load(Ordering::SeqCst), 1);
assert_eq!(lc.count(), 0);
}
/// Coverage: after a full cycle, a new cycle re-runs init and destroy (re-initialization works).
#[test]
fn reinitialization_runs_init_and_destroy_again() {
let inits = AtomicUsize::new(0);
let destroys = AtomicUsize::new(0);
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
for _ in 0..2 {
let ran_init = lc
.startup(|| -> Result<State, TestErr> {
inits.fetch_add(1, Ordering::SeqCst);
Ok(State { value: 1 })
})
.unwrap();
assert!(ran_init);
let ran_destroy = lc
.shutdown(|_s| {
destroys.fetch_add(1, Ordering::SeqCst);
})
.unwrap();
assert!(ran_destroy);
}
assert_eq!(inits.load(Ordering::SeqCst), 2);
assert_eq!(destroys.load(Ordering::SeqCst), 2);
assert_eq!(lc.count(), 0);
}
/// Coverage: `shutdown` with the count already at zero is an error (unbalanced call).
#[test]
fn shutdown_when_idle_is_error() {
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
assert!(lc.shutdown(|_| ()).is_err());
}
/// Coverage: `with_state` before any `startup` (and after the matching `shutdown`) is an error
/// because there is no active state.
#[test]
fn with_state_when_not_active_is_error() {
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
assert!(lc.with_state(|_s| ()).is_err());
lc.startup(|| -> Result<State, TestErr> { Ok(State { value: 5 }) })
.unwrap();
lc.shutdown(|_| ()).unwrap();
assert!(lc.with_state(|_s| ()).is_err());
}
/// Coverage: a failing init closure leaves the count at zero and the state absent, and the error is
/// propagated (type-erased into `Error::Init`).
#[test]
fn init_failure_keeps_count_zero_and_propagates_error() {
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
let r = lc.startup(|| -> Result<State, TestErr> { Err(TestErr::Boom) });
assert!(r.is_err());
assert_eq!(lc.count(), 0);
// State is absent, so accessing it errors.
assert!(lc.with_state(|_s| ()).is_err());
}
/// Coverage: `with_state` reads the stored state during an active lifecycle.
#[test]
fn with_state_reads_active_state() {
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
lc.startup(|| -> Result<State, TestErr> { Ok(State { value: 99 }) })
.unwrap();
let observed = lc.with_state(|s| s.value).unwrap();
assert_eq!(observed, 99);
lc.shutdown(|_| ()).unwrap();
}
@@ -1,3 +1,5 @@
# This file is created by sarasacw-omrf-packer and should not be changed manually.
{#
Utilize ${pcfiledir} to fetch the directory where current .pc file is.
And back to parent twice to obtain the installation directory.
@@ -1,3 +1,5 @@
# This file is created by sarasacw-omrf-packer and should not be changed manually.
{# Check whether this target is already exported -#}
if(TARGET {{ namespace }}::{{ target }})
message(STATUS "Target {{ namespace }}::{{ target }} is already defined.")
@@ -16,32 +18,45 @@ set(MyRust_LIBRARY "${_IMPORT_PREFIX}/lib/{{ artifact_dll }}")
set(MyRust_INCLUDE_DIR "${_IMPORT_PREFIX}/include")
{# Create modern CMake target (IMPORTED target) -#}
add_library({{ namespace }}::{{ target }} SHARED IMPORTED)
set_target_properties({{ namespace }}::{{ target }} PROPERTIES
add_library({{ target }} SHARED IMPORTED)
add_library({{ namespace }}::{{ target }} ALIAS {{ target }})
set_target_properties({{ target }} PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
)
{# Handle Windows and UNIX-like respectively -#}
if(WIN32)
set_target_properties({{ namespace }}::{{ target }} PROPERTIES
set_target_properties({{ target }} PROPERTIES
IMPORTED_LOCATION "${_IMPORT_PREFIX}/bin/{{ artifact_dll }}"
IMPORTED_IMPLIB "${_IMPORT_PREFIX}/lib/{{ artifact_lib }}"
)
else()
set_target_properties({{ namespace }}::{{ target }} PROPERTIES
set_target_properties({{ target }} PROPERTIES
IMPORTED_LOCATION "${_IMPORT_PREFIX}/lib/{{ artifact_dll }}"
)
endif()
{# Cleanup temporary variables -#}
set(_IMPORT_PREFIX)
{% if dependencies | length != 0 -%}
{# Pull in declared dependencies and wire them into the imported target -#}
include(CMakeFindDependencyMacro)
{% for dep in dependencies -%}
find_dependency({{ dep.package }})
{% endfor -%}
set_target_properties({{ namespace }}::{{ target }} PROPERTIES
set_target_properties({{ target }} PROPERTIES
INTERFACE_LINK_LIBRARIES "{{ dependencies | map(attribute='target') | join(';') }}"
)
{%- endif %}
{# Cleanup temporary variables -#}
set(_IMPORT_PREFIX)
{# Verify component (although there is no component) -#}
macro(check_required_components _NAME)
foreach(comp ${${_NAME}_FIND_COMPONENTS})
if(NOT ${_NAME}_${comp}_FOUND)
if(${_NAME}_FIND_REQUIRED_${comp})
set(${_NAME}_FOUND FALSE)
endif()
endif()
endforeach()
endmacro()
check_required_components({{ target }})