Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

modules: The 'features' module #11307

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Prev Previous commit
Next Next commit
DOC: Improve the overview, functions signatures
  • Loading branch information
seiko2plus committed Aug 10, 2023
commit 7768ba21e26936a80f7ee217838e6338c4c1fade
307 changes: 184 additions & 123 deletions docs/markdown/Features-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,24 @@

## Overview

Dealing with numerous CPU features through C and C++ compilers is a challenging task,
especially when aiming to support massive amount of CPU features for various architectures and multiple compilers
Additionally, supporting both baseline features and additional features dispatched at runtime presents another dilemma.
Dealing with a numerous of CPU features within C and C++ compilers poses intricate challenges,
particularly when endeavoring to support an extensive set of CPU features across diverse
architectures and multiple compilers. Furthermore, the conundrum of accommodating
both fundamental features and supplementary features dispatched at runtime
further complicates matters.

Another issue that may arise is simplifying the implementations of generic interfaces while keeping the dirty work laid
on the build system rather than using nested namespaces or recursive sources, relying on pragma or compiler targets attributes
on top of complicated precompiled macros or meta templates, which can make debugging and maintenance difficult.
In addition, the task of streamlining generic interface implementations often leads to the necessity
of intricate solutions, which can obscure code readability and hinder debugging and maintenance.
Nested namespaces, recursive sources, complex precompiled macros, or intricate meta templates
are commonly employed but can result in convoluted code structures.

While this module doesn't force you to follow a specific approach, it instead paves the way to count on a
practical multi-targets solution that can make managing CPU features easier and more reliable.
Enter the proposed module, which offers a simple pragmatic multi-target solution to
facilitate the management of CPU features with heightened ease and reliability.

In a nutshell, this module helps you deliver the following concept:
In essence, this module introduces the following core principle:

```C
// Brings the headers files of enabled CPU features
// Include header files for enabled CPU features
#ifdef HAVE_SSE
#include <xmmintrin.h>
#endif
Expand All @@ -32,20 +35,10 @@ In a nutshell, this module helps you deliver the following concept:
#ifdef HAVE_SSE41
#include <smmintrin.h>
#endif
#ifdef HAVE_POPCNT
#ifdef _MSC_VER
#include <nmmintrin.h>
#else
#include <popcntintrin.h>
#endif
#endif
#ifdef HAVE_AVX
#include <immintrin.h>
#endif

#ifdef HAVE_NEON
#include <arm_neon.h>
#endif
// ... (similar blocks for other features)

// MTARGETS_CURRENT defined as compiler argument via `features.multi_targets()`
#ifdef MTARGETS_CURRENT
Expand All @@ -55,19 +48,13 @@ In a nutshell, this module helps you deliver the following concept:
#define TARGET_SFX(X) X
#endif

// Core function utilizing feature-specific implementations
void TARGET_SFX(my_kernal)(const float *src, float *dst)
{
#ifdef HAVE_AVX512F
// defeintions for implied features alawys present
// no matter the compiler is
#ifndef HAVE_AVX2
#error "Alawys defined"
#endif
#elif defined(HAVE_AVX2) && defined(HAVE_FMA3)
#ifndef HAVE_AVX
#error "Alawys defined"
#endif
#elif defined(HAVE_SSE41)
// Feature-based branching
#if defined(HAVE_SSE41)
// Definitions for implied features always present,
// regardless of the compiler used.
#ifndef HAVE_SSSE3
#error "Alawys defined"
#endif
Expand All @@ -93,87 +80,107 @@ void TARGET_SFX(my_kernal)(const float *src, float *dst)
}
```

From the above code we can deduce the following:
- The code is written on top features based definitions rather than counting clusters or
features groups which gives the code more readability and flexibility.
Key Takeaways from the Code:

- Avoid using compiler built-in defeintions no matters the enabled arguments allows you
to easily manage the enabled/disabled features and to deal with any kind of compiler or features.
Since compilers like MSVC for example doesn't provides defeintions for all CPU features.
- The code employs feature-centric definitions, enhancing readability
and flexibility while sidestepping the need for feature grouping.

- The code is not aware of how its going to be build it, that gives the code a great prodiblity to
manage the generated objects which allow raising the baseline features at any time
or reduce and increase the additional dispatched features without changing the code.
- Notably, compiler-built definitions are circumvented, thereby affording
seamless management of enabled/disabled features and accommodating diverse compilers and feature sets.
Notably, this accommodates compilers like MSVC, which might lack definitions for specific CPU features.

- Allow building a single source multiple of times simplifying the implementations
of generic interfaces.
- The code remains agnostic about its build process, granting it remarkable versatility
in managing generated objects. This empowers the ability to elevate baseline features at will,
adjust additional dispatched features, and effect changes without necessitating code modifications.

- The architecture permits the construction of a singular source, which can be compiled multiple times.
This strategic approach simplifies the implementation of generic interfaces, streamlining the development process.

## Usage

## Usage
To use this module, just do: **`features = import('features')`**. The
following functions will then be available as methods on the object
with the name `features`. You can, of course, replace the name `features`
with anything else.

### features.new()

**Signature**
```meson
features.new(string, int,
implies: FeatureOject | FeatureObject[] = [],
group: string | string[] = [],
detect: string | {} | (string | {})[] = [],
args: string | {} | (string | {})[] = [],
test_code: string | File = '',
extra_tests: {string: string | file} = {},
disable: string = ''
) -> FeatureObject
FeatureObject features.new(
# Positional arguments:
str,
int,
# Keyword arguments:
implies : FeatureOject | list[FeatureObject] = [],
group : str | list[str] = [],
args : str | dict[str] | list[str | dict[str]] = [],
detect : str | dict[str] | list[str | dict[str]] = [],
test_code : str | File = '',
extra_tests : dict[str | file] = {},
disable : str = ''
)
```

This function plays a crucial role in the Features Module as it creates
a new `FeatureObject` instance that is essential for the functioning of
other methods within the module.

It takes two required positional arguments. The first one is the name of the feature,
and the second is the interest level of the feature, which is used by
the sort operation and priority of conflicting arguments.
The rest of the kwargs arguments are explained as follows:

* `implies` **FeatureOject | FeatureObject[] = []**: One or an array of features objects
representing predecessor features.

* `group` **string | string[] = []**: Optional one or an array of extra features names
to be added as extra definitions that can passed to source.

* `args` **string | {} | (string | {})[] = []**: Optional one or an array of compiler
arguments that are required to be enabled for this feature.
Each argument can be a string or a dictionary that holds four items that allow dealing
with the conflicts of the arguments of implied features:
- `val` **string**: string, the compiler argument.
- `match` **string | empty**: regex to match the arguments of implied features
that need to be filtered or erased.
- `mfilter` **string | empty**: regex to find certain strings from the matched arguments
to be combined with `val`. If the value of `mfilter` is empty or undefined,
any matches triggered by the value of `match` will not be combined with `val`.
- `mjoin` **string | empty**: a separator used to join all the filtered arguments.
If it's empty or undefined, the filtered arguments will be joined without a separator.

* `detect` **string | {} | (string | {})[] = []**: Optional one or an array of features names
that required to be detect on runtime. If no features sepecfied then the values of `group`
will be used if its provides otherwise the name of the feature will be used instead.
Similar to `args`, each feature name can be a string or a dictionary that holds four items
that allow dealing with the conflicts of the of implied features names.
See `features.multi_targets()` or `features.test()` for more clearfications.

* `test_code` **string | File = ''**: Optional C/C++ code or the path to the source
that needs to be tested against the compiler to consider this feature is supported.

* `extra_tests` **{string: string | file} = {}**: Optional dictionary holds extra tests where
the key represents the test name, which is also added as a compiler definition if the test succeeded,
and the value is C/C++ code or the path to the source that needs to be tested against the compiler.

* `disable` **string = ''**: Optional string to consider this feature disabled.

Returns a new instance of `FeatureObject`.
The `features.new()` function plays a pivotal role within the Features Module.
It creates a fresh mutable instance of FeatureObject, an essential component
for facilitating the functionality of other methods within the module.

This function requires two positional arguments. The first argument pertains to the feature's name,
while the second one involves the interest level of the feature. This interest level governs sorting
operations and aids in resolving conflicts among arguments with differing priorities.

Additional keyword arguments are elaborated as follows:

* `implies` **FeatureOject | list[FeatureObject]**:
An optional single feature object or an array of feature objects
representing predecessor features. It is noteworthy that two features can imply each other.
Such mutual implication can prove beneficial in addressing compatibility concerns with compilers or hardware.
For instance, while some compilers might require enabling both `AVX2` and `FMA3` simultaneously,
others may permit independent activation.

* `group` **str | list[str]**:
An optional single feature name or an array of additional feature names. These names are appended as
supplementary definitions that can be passed to the source.

* `args` **str | dict[str] | list[str | dict[str]] = []**:
An optional single compiler argument or an array of compiler arguments that must be enabled for
the corresponding feature. Each argument can be a string or a dictionary containing four elements.
These elements handle conflicts arising from arguments of implied features or when concatenating two features:
- `val` **str**:
The compiler argument.
- `match` **str | empty**:
A regular expression to match arguments of implied features that necessitate filtering or removal.
- `mfilter` **str | empty**:
A regular expression to identify specific strings from matched arguments.
These strings are combined with `val`. If `mfilter` is empty or undefined,
matched arguments from `match` will not be combined with `val`.
- `mjoin` **str | empty**:
A separator used to join all filtered arguments.
If undefined or empty, filtered arguments are joined without a separator.

* `detect` **str | dict[str] | list[str | dict[str]] = [] = []**:
An optional single feature name or an array of feature names to be detected at runtime.
If no feature names are specified, the values from the `group` will be used.
If the `group` doesn't provide values, the feature's name is employed instead.
Similar to args, each feature name can be a string or a dictionary with four
elements to manage conflicts of implied feature names.
Refer to `features.multi_targets()` or `features.test()` for further clarity.

* `test_code` **str | File = ''**:
An optional block of C/C++ code or the path to a source file for testing against the compiler.
Successful compilation indicates feature support.

* `extra_tests` **dict[str | file] = {}**:
An optional dictionary containing extra tests. The keys represent test names,
and the associated values are C/C++ code or paths to source files.
Successful tests lead to compiler definitions based on the test names.

* `disable` **str = ''**:
An optional string to mark a feature as disabled.
If the string is empty or undefined, the feature is considered not disabled.

The function returns a new instance of `FeatureObject`.

Example:

Expand Down Expand Up @@ -211,49 +218,103 @@ ASIMDFHM = features.new(
args: {'val': '-march=armv8.2-a+fp16fml', 'match': '-march=.*', 'mfilter': '\+.*'}
)
```

### features.test()
**Signature**
```meson
features.test(FeatureObject...,
anyfet: bool = false,
force_args: string | string[] | empty = empty,
compiler: Compiler | empty = empty,
cached: bool = true,
) -> {}
[bool, Dict] features.test(
# Positional arguments:
FeatureObject...
# Keyword arguments:
anyfet : bool = false,
force_args : str | list[str] | empty = empty,
compiler : Compiler | empty = empty,
cached : bool = true
)
```

Test a one or set of features against the compiler and returns a dictionary
contains all required information that needed for building a source that
requires these features.
Test one or a list of features against the compiler and retrieve a dictionary containing
essential information required for compiling a source that depends on these features.

A feature is deemed supported if it fulfills the following criteria:

- It is not marked as disabled.
- The compiler accommodates the features arguments.
- Successful compilation of the designated test file or code.
- The implied features are supported by the compiler, aligning with
the criteria mentioned above.

This function requires at least one feature object as positional argument,
and additional keyword arguments are elaborated as follows:

- `anyfet` **bool = false**:
If set to true, the returned dictionary will encompass information regarding
the maximum features available that are supported by the compiler.
This extends beyond the specified features and includes their implied features.
- `force_args` **str | list[str] | empty = empty**:
An optional single compiler argument or an array of compiler arguments to be
employed instead of the designated features' arguments when testing the
test code against the compiler. This can be useful for detecting host features.
- `compiler` **Compiler | empty = empty**:
A Compiler object to be tested against. If not defined,
the function will default to the standard C or C++ compiler.
- `cached`: **bool = true**:
Enable or disable the cache. By default, the cache is enabled,
enhancing efficiency by storing previous results.

This function returns a list of two items. The first item is a boolean value,
set to true if the compiler supports the feature and false if it does not.
The second item is a dictionary that encapsulates the test results, outlined as follows:

**structure**
```meson
{
'target_name' : str,
'prevalent_features' : list[str],
'features' : list[str],
'args' : list[str],
'detect' : list[str],
'defines' : list[str],
'undefines' : list[str],
'is_supported' : bool,
'is_disabled' : bool,
'fail_reason' : str
}
```

### features.multi_targets()
**Signature**
```meson
features.multi_targets(string, (
str | File | CustomTarget | CustomTargetIndex |
GeneratedList | StructuredSources | ExtractedObjects |
BuildTarget
)...,
dispatch: (FeatureObject | FeatureObject[])[] = [],
baseline: empty | FeatureObject[] = empty,
prefix: string = '',
compiler: empty | compiler = empty,
cached: bool = True
) [{}[], StaticLibrary[]]
TargetsObject features.multi_targets(
# Positional arguments:
str,
(
str | File | CustomTarget | CustomTargetIndex |
GeneratedList | StructuredSources | ExtractedObjects |
BuildTarget
)...,
# Keyword arguments:
dispatch : FeatureObject | list[FeatureObject] = [],
baseline : empty | list[FeatureObject] = empty,
prefix : str = '',
keep_sort : bool = false,
compiler : empty | compiler = empty,
cached : bool = True,
**known_stlib_kwargs
)
```


### features.sort()
**Signature**
```meson
features.sort(FeatureObject..., reverse: bool = false) : FeatureObject[]
```

### features.implicit()
**Signature**
```meson
features.implicit(FeatureObject...) : FeatureObject[]
```

### features.implicit_c()
**Signature**
```meson
features.implicit_c(FeatureObject...) : FeatureObject[]
```

Loading