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

Experimental clib from doxygen #1835

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7ca7284
[SCons] Enable clib_experimental flag
ischoegl Jan 2, 2025
38c311e
[doxygen] Exclude clib_experimental paths
ischoegl Jan 2, 2025
d7ace1d
[sourcegen] Switch sourcegen utility to argparse
ischoegl Dec 29, 2024
0be764c
[sourcegen] Add option for CLib
ischoegl Dec 28, 2024
15e3cb7
[sourcegen] Add YAML header configurations
ischoegl Dec 28, 2024
8415326
[sourcegen] Parse tagfile and add jinja templates
ischoegl Dec 28, 2024
24cf383
[sourcegen] Get doxygen tag information
ischoegl Dec 28, 2024
ae0bbd8
[sourcegen] Add doxygen tag lookup
ischoegl Dec 28, 2024
e4ec298
[sourcegen] Add CLib crosswalks and output
ischoegl Dec 28, 2024
3abf76a
[sourcegen] Generate headers via YAML
ischoegl Dec 29, 2024
6aef1db
[sourcegen] Annotated Func objects
ischoegl Dec 29, 2024
9660975
[sourcegen] Add YAML scaffolder for debugging
ischoegl Dec 29, 2024
4f70717
[sourcegen] Generate CLib implementation files
ischoegl Dec 30, 2024
1bd6828
[sourcegen] Improve data flow and logger output
ischoegl Dec 30, 2024
18ff3c9
[sourcegen] Improve HeaderFileParser method names
ischoegl Dec 30, 2024
03f4d4e
[sourcegen] Improve location of constructors
ischoegl Dec 30, 2024
e0a5173
[sourcegen] Add common CLib definitions
ischoegl Dec 30, 2024
3ae9a64
[sourcegen] Generate simple CLib getters/setters
ischoegl Dec 30, 2024
223edd4
[sourcegen] Improve autodetection based on YAML input
ischoegl Dec 30, 2024
6de7a36
[sourcegen] Improve setter/getter code generation
ischoegl Dec 31, 2024
8d85de3
[sourcegen] Simplify recipes
ischoegl Dec 31, 2024
7251e91
[sourcegen] Add remaining CLib scaffolders
ischoegl Dec 31, 2024
8b90d4f
[sourcegen] Simplify code and templates
ischoegl Dec 31, 2024
9374e3c
[sourcegen] Add includes
ischoegl Jan 1, 2025
19867fd
[sourcegen] Add service methods
ischoegl Jan 1, 2025
796367a
[sourcegen] Add main CLib header
ischoegl Jan 1, 2025
4ac3a0f
[sourcegen] Add verbosity option to sourcegen
ischoegl Jan 1, 2025
10cea66
[sourcegen] Improve YAML source generator
ischoegl Jan 1, 2025
9771edc
[sourcegen] Improve CLib source generator
ischoegl Jan 1, 2025
a3b8e1b
[sourcegen] Implement reserved CLib methods
ischoegl Jan 1, 2025
c0ba0d5
[sourcegen] Improve nomenclature and fix glitches
ischoegl Jan 1, 2025
5a0c322
[sourcegen] Document/simplify YAML specifications
ischoegl Jan 2, 2025
ed2f2de
[sourcegen] Use xml.etree to parse XML
ischoegl Jan 2, 2025
4a7f97f
[sourcegen] Add vector<shared_ptr<T>> handling
ischoegl Jan 4, 2025
d2729d6
[sourcegen] Handle custom code
ischoegl Jan 5, 2025
b081f1e
[unittests] Add googletests for clib_experimental
ischoegl Jan 2, 2025
95f0701
[sourcegen] Improve type hinting
ischoegl Jan 4, 2025
19ca9a4
[CI] Add clib_experimental tests to workflow
ischoegl Jan 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,11 @@ jobs:
run: |
sudo apt update
sudo apt install libboost-dev gfortran libomp-dev libomp5 \
libopenblas-openmp-dev libhdf5-dev
libopenblas-openmp-dev libhdf5-dev doxygen graphviz
- name: Upgrade pip
run: python3 -m pip install -U pip setuptools wheel
- name: Install Python dependencies
run: python3 -m pip install ruamel.yaml scons numpy cython pandas pytest
run: python3 -m pip install ruamel.yaml scons numpy cython pandas pytest Jinja2
pytest-xdist pytest-github-actions-annotate-failures pint graphviz
- name: Build Cantera
run: python3 `which scons` build env_vars=all
Expand All @@ -177,6 +177,13 @@ jobs:
run: |
LD_LIBRARY_PATH=build/lib
python3 -m pytest -raP -v -n auto --durations=50 test/python
- name: Generate clib_experimental
run: |
python3 `which scons` doxygen
python3 interfaces/sourcegen/run.py --api=clib --output=.
python3 `which scons` build clib_experimental=y
- name: Run googletests for clib_experimental
run: python3 `which scons` test-clib-experimental --debug=time

macos-multiple-pythons:
name: ${{ matrix.macos-version }} with Python ${{ matrix.python-version }}
Expand Down Expand Up @@ -893,9 +900,16 @@ jobs:
steps:
- uses: actions/checkout@v4
name: Checkout the repository
- name: Override default Python (Windows)
# Other operating systems use default system Python 3
uses: actions/setup-python@v5
with:
python-version: "3.10"
architecture: x64
if: matrix.os == 'windows-2022'
- name: Install Python dependencies
# Install for the default system Python 3, which is used by 'dotnet build'
run: python3 -m pip install ruamel.yaml Jinja2
# Install Python dependencies, which are used by 'dotnet build'
run: python3 -m pip install ruamel.yaml Jinja2 typing-extensions
- name: Install library dependencies with micromamba (Windows)
uses: mamba-org/setup-micromamba@v1
with:
Expand Down
8 changes: 8 additions & 0 deletions SConstruct
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ if "clean" in COMMAND_LINE_TARGETS:
remove_file(name)
for name in Path("site_scons").glob("**/*.pyc"):
remove_file(name)
for name in Path("include/cantera/clib_experimental").glob("*.h"):
remove_file(name)
for name in Path("src/clib_experimental").glob("*.cpp"):
remove_file(name)

logger.status("Done removing output files.", print_level=False)

Expand Down Expand Up @@ -364,6 +368,10 @@ config_options = [
"sphinx_docs",
"Build HTML documentation for Cantera using Sphinx.",
False),
BoolOption(
"clib_experimental",
"Build experimental CLib.",
False),
BoolOption(
"run_examples",
"""Run examples to generate plots and outputs for Sphinx Gallery. Disable to
Expand Down
3 changes: 2 additions & 1 deletion doc/doxygen/Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -1013,7 +1013,8 @@ RECURSIVE = YES
# Note that relative paths are relative to the directory from which doxygen is
# run.

EXCLUDE = include/cantera/ext
EXCLUDE = include/cantera/ext \
include/cantera/clib_experimental

# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
# directories that are symbolic links (a Unix file system feature) are excluded
Expand Down
1 change: 1 addition & 0 deletions include/cantera/clib_experimental/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.h
25 changes: 25 additions & 0 deletions include/cantera/clib_experimental/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Cantera – Experimental CLib Interface

This directory and the associated `src/clib_experimental` folder contain an
experimental re-implementation of Cantera's traditional CLib interface.

## Code Generation

Run the following command from the Cantera root folder:

```
scons doxygen
python3 interfaces/sourcegen/run.py --api=clib --output=.
scons build clib_experimental=y
```

A rudimentary test suite ensures that code performs as expected:

```
scons test-clib-experimental
```

## Status

The experimental CLib Interface is in preview and still missing many features
needed for parity with the traditional CLib interface.
2 changes: 1 addition & 1 deletion interfaces/dotnet/Cantera/Cantera.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
<GeneratedPath>$([MSBuild]::NormalizePath($(IntermediateOutputPath)/sourcegen/))</GeneratedPath>
</PropertyGroup>

<Exec Command="python3 $(Script) csharp $(GeneratedPath)"
<Exec Command="python3 $(Script) --api=csharp --output=$(GeneratedPath)"
Condition="'$(GenerateInterop)' == 'true' Or !Exists('$(GeneratedPath)')"/>

<ItemGroup>
Expand Down
12 changes: 5 additions & 7 deletions interfaces/sourcegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

The `sourcegen.generate_source` function crudely parses the CLib header files and generates intermediate objects which represent the functions:
* header file path
* funcs: list of
* return type (string)
* name (string)
* params: list of
* return type
* name
* funcs: list of `Func` objects containing
* return type (`string`)
* name (`string`)
* params: list of function arguments (`ArgList`)
* optional annotations

`sourcegen.generate_source` then delegates the source generation to a language-specific sub-package. The sub-package creates the source files in a location appropriate to the build for that language’s build process.

Expand All @@ -34,4 +33,3 @@ Each sub-package can contain a yaml-based config file named `config.yaml`. The c
The config file may contain additional values for use by the language-specific sub-package.

## running from the command line

44 changes: 43 additions & 1 deletion interfaces/sourcegen/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,48 @@
# at https://cantera.org/license.txt for license and copyright information.

import sys
import argparse
import textwrap

import sourcegen

sourcegen.generate_source(*sys.argv[1:])
def main(argv=None):
parser = create_argparser()
if argv is None and len(sys.argv) < 2:
parser.print_help(sys.stderr)
sys.exit(1)
args = parser.parse_args(argv)
lang = args.api
output = args.output
verbose = args.verbose
sourcegen.generate_source(lang, output, verbose=verbose)

def create_argparser():
parser = argparse.ArgumentParser(
description=(
"Experimental source generator for creating Cantera interface code."),
epilog=textwrap.dedent(
"""
The **sourcegen** utility is invoked as follows::

python path/to/sourcegen/run.py --api=csharp --output=.

where the relative path has to be provided as the utility is not installed.
Currently supported API options are: 'csharp', 'clib' and 'yaml'.
"""),
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
"-v", "--verbose", action="store_true", default=False,
help="show additional logging output")
parser.add_argument(
"--api", default="",
help="language of generated Cantera API code")
parser.add_argument(
"--output", default="",
help="specifies the OUTPUT folder name")

return parser

if __name__ == "__main__":
main()
97 changes: 84 additions & 13 deletions interfaces/sourcegen/sourcegen/_HeaderFileParser.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,102 @@
"""Parser for existing CLib headers."""
"""Parser for YAML header configurations or existing CLib headers."""

# This file is part of Cantera. See License.txt in the top-level directory or
# at https://cantera.org/license.txt for license and copyright information.

from pathlib import Path
import logging
import re
from typing import List
from typing import Iterable
from typing_extensions import Self

from ._dataclasses import HeaderFile, Func
from ._dataclasses import HeaderFile, Func, Recipe
from ._helpers import read_config


_logger = logging.getLogger()
_LOGGER = logging.getLogger()

_clib_path = Path(__file__).parent.joinpath("../../../include/cantera/clib").resolve()
_clib_ignore = ["clib_defs.h", "ctmatlab.h"]
_CLIB_PATH = Path(__file__).parents[3] / "include" / "cantera" / "clib"
_CLIB_IGNORE = ["clib_defs.h", "ctmatlab.h"]

_DATA_PATH = Path(__file__).parent / "_data"

class HeaderFileParser:
"""
Parser for header files or corresponding YAML specifications.

Provides for convenience methods to generate lists of `HeaderFile` objects, which
themselves are used for subsequent code scaffolding.
"""

def __init__(self, path: Path, ignore_funcs: List[str] = None):
def __init__(self, path: Path, ignore_funcs: Iterable[str] = None) -> None:
self._path = path
self._ignore_funcs = ignore_funcs

@classmethod
def from_headers(cls, ignore_files, ignore_funcs) -> List[HeaderFile]:
def headers_from_yaml(
cls: Self, ignore_files: Iterable[str], ignore_funcs: Iterable[str]
) -> list[HeaderFile]:
"""Parse header file YAML configuration."""
files = sorted(
ff for ff in _DATA_PATH.glob("*.yaml") if ff.name not in ignore_files)
return [cls(ff, ignore_funcs.get(ff.name, []))._parse_yaml() for ff in files]

def _parse_yaml(self) -> HeaderFile:
def read_docstring():
doc = []
with self._path.open("r", encoding="utf-8") as fid:
while True:
line = fid.readline()
if line.startswith("#"):
doc.append(line.removeprefix("#").strip())
else:
break
if doc and doc[0].startswith("This file is part of "):
return []
return doc

msg = f" parsing {self._path.name!r}"
_LOGGER.info(msg)
config = read_config(self._path)
if self._ignore_funcs:
msg = f" ignoring {self._ignore_funcs!r}"
_LOGGER.info(msg)

recipes = []
prefix = config["prefix"]
base = config["base"]
parents = config.get("parents", [])
derived = config.get("derived", [])
for recipe in config["recipes"]:
if recipe["name"] in self._ignore_funcs:
continue
uses = recipe.get("uses", [])
if not isinstance(uses, list):
uses = [uses]
recipes.append(
Recipe(recipe["name"],
recipe.get("implements", ""),
uses,
recipe.get("what", ""),
recipe.get("brief", ""),
recipe.get("code", ""),
prefix,
base,
parents,
derived))

return HeaderFile(self._path, [], prefix, base, parents, derived, recipes,
read_docstring())

@classmethod
def headers_from_h(
cls: Self, ignore_files: Iterable[str], ignore_funcs: Iterable[str]
) -> list[HeaderFile]:
"""Parse existing header file."""
files = [_ for _ in _clib_path.glob("*.h")
if _.name not in ignore_files + _clib_ignore]
return [cls(_, ignore_funcs.get(_.name, []))._parse_h() for _ in files]
files = [ff for ff in _CLIB_PATH.glob("*.h")
if ff.name not in ignore_files + _CLIB_IGNORE]
files.sort()
return [cls(ff, ignore_funcs.get(ff.name, []))._parse_h() for ff in files]

def _parse_h(self) -> HeaderFile:
ct = self._path.read_text()
Expand All @@ -41,9 +110,11 @@ def _parse_h(self) -> HeaderFile:

parsed = map(Func.from_str, c_functions)

_logger.info(f" parsing {self._path.name!r}")
msg = f" parsing {self._path.name!r}"
_LOGGER.info(msg)
if self._ignore_funcs:
_logger.info(f" ignoring {self._ignore_funcs!r}")
msg = f" ignoring {self._ignore_funcs!r}"
_LOGGER.info(msg)

parsed = [f for f in parsed if f.name not in self._ignore_funcs]

Expand Down
5 changes: 2 additions & 3 deletions interfaces/sourcegen/sourcegen/_SourceGenerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from abc import ABCMeta, abstractmethod
from pathlib import Path
from typing import List

from ._dataclasses import HeaderFile

Expand All @@ -14,9 +13,9 @@ class SourceGenerator(metaclass=ABCMeta):
"""Specifies the interface of a language-specific SourceGenerator"""

@abstractmethod
def __init__(self, out_dir: Path, config: dict):
def __init__(self, out_dir: Path, config: dict, templates: dict) -> None:
pass

@abstractmethod
def generate_source(self, headers_files: List[HeaderFile]):
def generate_source(self, headers_files: list[HeaderFile]) -> None:
pass
Loading
Loading