Created
June 14, 2025 20:54
-
-
Save kassane/5b8bf8fabfe7ea83368848353694ea9f to your computer and use it in GitHub Desktop.
Clang-ast-dump: convert C++ to D
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import json | |
| import subprocess | |
| import sys | |
| import argparse | |
| import re | |
| from typing import Dict, List, Optional | |
| from pathlib import Path | |
| import datetime | |
| import os | |
| # Counter for anonymous struct names | |
| _anon_counter = 0 | |
| def get_clang_include_paths() -> List[str]: | |
| """Detect standard C++ include paths using clang++.""" | |
| timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| cmd = ['clang++', '-E', '-x', 'c++', '-', '-v'] | |
| try: | |
| result = subprocess.run( | |
| cmd, | |
| input='', | |
| capture_output=True, | |
| text=True, | |
| check=True | |
| ) | |
| include_paths = [] | |
| include_section = False | |
| for line in result.stderr.splitlines(): | |
| line = line.strip() | |
| if line.startswith('#include <...> search starts here:'): | |
| include_section = True | |
| continue | |
| if line.startswith('End of search list.'): | |
| include_section = False | |
| continue | |
| if include_section and os.path.isdir(line): | |
| include_paths.append(line) | |
| return include_paths | |
| except subprocess.CalledProcessError as e: | |
| with open('clang_stderr.log', 'a') as f: | |
| f.write(f"[{timestamp}] Error detecting include paths: {e}\n") | |
| f.write(f"[{timestamp}] STDERR:\n{e.stderr}\n") | |
| return [] | |
| def clang(csrc_path: str, include_paths: List[str] = [], stdout_log: str = "clang_stdout.log", stderr_log: str = "clang_stderr.log") -> Dict: | |
| """Run Clang to generate AST dump in JSON format, logging stdout and stderr separately.""" | |
| # Delete previous log files | |
| for log_file in [stdout_log, stderr_log]: | |
| if os.path.exists(log_file): | |
| os.remove(log_file) | |
| if not os.path.isfile(csrc_path): | |
| with open(stderr_log, 'a') as f: | |
| timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| f.write(f"[{timestamp}] Error: {csrc_path} does not exist or is not a file\n") | |
| print(f"Error: {csrc_path} does not exist") | |
| sys.exit(1) | |
| auto_includes = get_clang_include_paths() | |
| cmd = ['clang', '-Xclang', '-ast-dump=json', '-c', csrc_path, '-fparse-all-comments'] | |
| for path in set(auto_includes + include_paths): | |
| if os.path.isdir(path): | |
| cmd.append(f'-I{path}') | |
| timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| try: | |
| result = subprocess.run( | |
| cmd, | |
| capture_output=True, | |
| text=True, | |
| check=True | |
| ) | |
| try: | |
| ast_data = json.loads(result.stdout) | |
| except json.JSONDecodeError as e: | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] JSON Parse Error: {e}\n") | |
| f.write(f"[{timestamp}] STDOUT:\n{result.stdout}\n") | |
| f.write(f"[{timestamp}] STDERR:\n{result.stderr}\n") | |
| print(f"Error parsing Clang JSON output, see {stderr_log} for details") | |
| sys.exit(1) | |
| if not ast_data.get('inner'): | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Warning: Empty AST; check input file or include paths\n") | |
| with open(stdout_log, 'a') as f: | |
| f.write(f"[{timestamp}] Command: {' '.join(cmd)}\n") | |
| f.write(f"[{timestamp}] STDOUT:\n{result.stdout}\n") | |
| if result.stderr: | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Command: {' '.join(cmd)}\n") | |
| f.write(f"[{timestamp}] STDERR:\n{result.stderr}\n") | |
| return ast_data | |
| except subprocess.CalledProcessError as e: | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Command: {' '.join(cmd)}\n") | |
| f.write(f"[{timestamp}] ERROR: Command returned non-zero exit status {e.returncode}\n") | |
| f.write(f"[{timestamp}] STDOUT:\n{e.stdout}\n") | |
| f.write(f"[{timestamp}] STDERR:\n{e.stderr}\n") | |
| print(f"Error running Clang, see {stderr_log} for details") | |
| sys.exit(1) | |
| def detect_module(csrc_path: str, stderr_log: str) -> str: | |
| """Detect module name from file name.""" | |
| timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| module = Path(csrc_path).stem | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Detected module: {module}\n") | |
| return module | |
| def get_d_type(cpp_type: str, stderr_log: str, timestamp: str) -> str: | |
| """Map C++ types to D types.""" | |
| type_map = { | |
| 'int': 'int', | |
| 'double': 'double', | |
| 'float': 'float', | |
| 'char': 'char', | |
| 'void': 'void', | |
| 'bool': 'bool', | |
| 'unsigned int': 'uint', | |
| 'unsigned char': 'ubyte', | |
| 'unsigned short': 'ushort', | |
| 'unsigned long': 'ulong', | |
| 'signed char': 'byte', | |
| 'short': 'short', | |
| 'long': 'long', | |
| '__int128_t': 'cent', | |
| '__uint128_t': 'ucent', | |
| 'std::allocator': 'allocator', | |
| 'std::string': 'string', | |
| 'std::vector': 'vector', | |
| 'std::basic_string': 'basic_string' | |
| } | |
| if cpp_type.startswith('std::vector<'): | |
| match = re.match(r'std::vector<([^,>]+)(?:,\s*([^>]+))?>', cpp_type) | |
| if match: | |
| t_type = get_d_type(match.group(1).strip(), stderr_log, timestamp) | |
| return f"vector!{t_type}" | |
| if cpp_type.endswith('*'): | |
| return f"{get_d_type(cpp_type[:-1], stderr_log, timestamp)}*" | |
| if cpp_type.endswith('&'): | |
| return get_d_type(cpp_type[:-1], stderr_log, timestamp) | |
| if cpp_type.endswith('[]'): | |
| return f"{get_d_type(cpp_type[:-2], stderr_log, timestamp)}[]" | |
| if ('std::' in cpp_type or 'std::pmr::' in cpp_type) and cpp_type not in type_map: | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Warning: Unmapped standard library type {cpp_type} mapped directly\n") | |
| return cpp_type.replace('std::', '').replace('std::pmr::', '') | |
| return type_map.get(cpp_type, cpp_type) | |
| def process_typedef(node: Dict, namespace: Optional[str], source: str, stderr_log: str, timestamp: str) -> Dict: | |
| """Generate IR for typedef or using declarations.""" | |
| if node.get('kind') not in ['TypedefDecl', 'TypeAliasDecl']: | |
| return {} | |
| name = node.get('name', '') | |
| underlying_type = get_d_type(node.get('type', {}).get('qualType', ''), stderr_log, timestamp) | |
| # Skip problematic aliases | |
| if name in ['__int128_t', '__uint128_t', '__NSConstantString', '__builtin_ms_va_list', '__builtin_va_list']: | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Skipped typedef {name} to avoid invalid D alias\n") | |
| return {} | |
| if not name or not underlying_type or 'std' in (namespace or '') or 'std::' in underlying_type: | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Skipped typedef {name} due to std namespace or type\n") | |
| return {} | |
| outp = { | |
| 'kind': 'alias', | |
| 'name': name, | |
| 'type': underlying_type, | |
| 'namespace': namespace | |
| } | |
| for inner in node.get('inner', []): | |
| if inner.get('kind') == 'FullComment': | |
| comment = source[inner['range']['begin']['offset']:inner['range']['end']['offset']+1].rstrip() | |
| outp['comment'] = comment | |
| return outp | |
| def process_function(node: Dict, namespace: Optional[str], source: str, stderr_log: str, timestamp: str) -> Dict: | |
| """Generate IR for function or method declarations.""" | |
| if node.get('kind') not in ['FunctionDecl', 'CXXMethodDecl', 'CXXConstructorDecl', 'CXXDestructorDecl']: | |
| return {} | |
| name = node.get('name', '') | |
| mangled_name = node.get('mangledName', '') | |
| qual_type = node.get('type', {}).get('qualType', '') | |
| is_constructor = node.get('kind') == 'CXXConstructorDecl' | |
| is_destructor = node.get('kind') == 'CXXDestructorDecl' | |
| if (name.startswith('operator') or 'std' in (namespace or '') or | |
| 'std::' in qual_type or 'std::pmr::' in qual_type): | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Skipped function {name} due to std namespace or type\n") | |
| return {} | |
| return_type = 'void' if is_constructor or is_destructor else get_d_type( | |
| qual_type.split('(')[0].strip(), stderr_log, timestamp) | |
| params = [] | |
| # All functions/methods are @nogc and @safe by default, nothrow if noexcept | |
| attributes = ['@nogc', '@safe'] | |
| if node.get('isNoexcept', False): | |
| attributes.append('nothrow') | |
| if node.get('isVirtual', False): | |
| attributes.append('abstract') | |
| for inner_node in node.get('inner', []): | |
| if inner_node.get('kind') == 'ParmVarDecl': | |
| param_type = get_d_type(inner_node.get('type', {}).get('qualType', ''), stderr_log, timestamp) | |
| if param_type.endswith('...'): | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Warning: Skipping function {name} with variadic parameters\n") | |
| return {} | |
| param_name = inner_node.get('name', f"param{len(params)}") | |
| params.append({'type': param_type, 'name': param_name}) | |
| elif inner_node.get('kind') == 'FullComment': | |
| comment = source[inner_node['range']['begin']['offset']:inner_node['range']['end']['offset']+1].rstrip() | |
| outp = { | |
| 'kind': 'func' if not is_constructor and not is_destructor else 'constructor' if is_constructor else 'destructor', | |
| 'name': name, | |
| 'return': return_type, | |
| 'params': params, | |
| 'namespace': namespace, | |
| 'mangled': mangled_name if mangled_name and mangled_name.startswith('_Z') else None, | |
| 'attributes': attributes | |
| } | |
| if 'comment' in locals(): | |
| outp['comment'] = comment | |
| return outp | |
| def process_record(node: Dict, namespace: Optional[str], source: str, stderr_log: str, timestamp: str) -> Dict: | |
| """Generate IR for class or struct declarations, avoiding anonymous unions.""" | |
| global _anon_counter | |
| if node.get('kind') not in ['CXXRecordDecl']: | |
| return {} | |
| tag = node.get('tagUsed', '') | |
| name = node.get('name', '') | |
| if not name and tag == 'struct': | |
| name = f"AnonStruct_{_anon_counter}" | |
| _anon_counter += 1 | |
| if (not name or name in ['vector', 'memory_resource', 'allocator', 'basic_string'] or | |
| 'std' in (namespace or '')): | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Skipped record {name} due to std namespace or standard type\n") | |
| return {} | |
| is_class = tag == 'class' | |
| has_virtual = any(inner.get('isVirtual', False) for inner in node.get('inner', []) if inner.get('kind') == 'CXXMethodDecl') | |
| has_constructors = any(inner.get('kind') == 'CXXConstructorDecl' for inner in node.get('inner', [])) | |
| # Map to D class if C++ class or has virtual methods/constructors | |
| d_type = 'class' if is_class or has_virtual or has_constructors else 'struct' | |
| fields = [] | |
| methods = [] | |
| aliases = [] | |
| is_incomplete = not node.get('completeDefinition', False) | |
| if is_incomplete: | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Warning: Processing incomplete definition for {name}\n") | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Processing record {name} as D {d_type}, has_constructors={has_constructors}\n") | |
| for inner_node in node.get('inner', []): | |
| if inner_node.get('kind') == 'FieldDecl': | |
| field_type = get_d_type(inner_node.get('type', {}).get('qualType', ''), stderr_log, timestamp) | |
| field_name = inner_node.get('name', '') | |
| if field_name and 'std::' not in field_type and 'std::pmr::' not in field_type: | |
| field_comment = None | |
| for sub_inner in inner_node.get('inner', []): | |
| if sub_inner.get('kind') == 'FullComment': | |
| field_comment = source[sub_inner['range']['begin']['offset']:sub_inner['range']['end']['offset']+1].rstrip() | |
| fields.append({'type': field_type, 'name': field_name, 'comment': field_comment}) | |
| elif inner_node.get('kind') == 'CXXRecordDecl' and not inner_node.get('name'): | |
| anon_tag = inner_node.get('tagUsed', '') | |
| if anon_tag == 'struct': | |
| field_name = inner_node.get('fieldName', f"anon_struct_{_anon_counter}") | |
| _anon_counter += 1 | |
| if field_name: | |
| anon_fields = [] | |
| for anon_inner in inner_node.get('inner', []): | |
| if anon_inner.get('kind') == 'FieldDecl': | |
| anon_type = get_d_type(anon_inner.get('type', {}).get('qualType', ''), stderr_log, timestamp) | |
| anon_name = anon_inner.get('name', '') | |
| if anon_name and 'std::' not in anon_type and 'std::pmr::' not in anon_type: | |
| anon_fields.append({'type': anon_type, 'name': anon_name}) | |
| if anon_fields: | |
| fields.append({'type': 'struct', 'name': field_name, 'fields': anon_fields}) | |
| elif anon_tag == 'union': | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Skipped anonymous union in {name}\n") | |
| continue | |
| elif inner_node.get('kind') in ['CXXMethodDecl', 'CXXConstructorDecl', 'CXXDestructorDecl']: | |
| if not inner_node.get('isStatic', False): | |
| method = process_function(inner_node, namespace, source, stderr_log, timestamp) | |
| if method: | |
| methods.append(method) | |
| elif inner_node.get('kind') == 'FullComment': | |
| comment = source[inner_node['range']['begin']['offset']:inner_node['range']['end']['offset']+1].rstrip() | |
| elif inner_node.get('kind') in ['TypedefDecl', 'TypeAliasDecl']: | |
| typedef = process_typedef(inner_node, namespace, source, stderr_log, timestamp) | |
| if typedef: | |
| aliases.append(typedef) | |
| if is_incomplete and not (fields or methods or aliases): | |
| return {'kind': d_type, 'name': name, 'namespace': namespace, 'is_incomplete': True} | |
| if not is_incomplete and not (fields or methods or aliases): | |
| return {} | |
| outp = { | |
| 'kind': d_type, | |
| 'name': name, | |
| 'fields': fields, | |
| 'methods': methods, | |
| 'aliases': aliases, | |
| 'namespace': namespace, | |
| 'is_incomplete': is_incomplete | |
| } | |
| if 'comment' in locals(): | |
| outp['comment'] = comment | |
| return outp | |
| def detect_std_types(ast: Dict) -> set: | |
| """Detect standard library types and 128-bit integer types used in the AST, excluding std::pmr.""" | |
| std_types = set() | |
| def check_nodes(nodes: List[Dict]): | |
| for node in nodes: | |
| if node.get('kind') == 'TypeRef': | |
| qual_type = node.get('type', {}).get('qualType', '') | |
| if qual_type.startswith('std::') and not qual_type.startswith('std::pmr::'): | |
| std_types.add(qual_type.split('<')[0]) | |
| elif qual_type in ['__int128_t', '__uint128_t']: | |
| std_types.add(qual_type) | |
| elif node.get('kind') == 'CXXRecordDecl': | |
| ns = node.get('namespace', '') | |
| if ns and ns.startswith('std') and not ns.startswith('std::pmr'): | |
| std_types.add(f"std::{node.get('name', '')}") | |
| check_nodes(node.get('inner', [])) | |
| check_nodes(ast.get('inner', [])) | |
| return std_types | |
| def generate_ir(csrc_path: str, include_paths: List[str], stdout_log: str, stderr_log: str, json_file: str, source: str) -> Dict: | |
| """Generate intermediate representation from Clang AST.""" | |
| ast = clang(csrc_path, include_paths, stdout_log, stderr_log) | |
| timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| module = detect_module(csrc_path, stderr_log) | |
| std_types = detect_std_types(ast) | |
| outp = { | |
| 'module': module, | |
| 'decls': [], | |
| 'header_comment': '', | |
| 'license': '', | |
| 'authors': '', | |
| 'uses_memory': bool(re.search(r'#include\s*<memory>', source)), | |
| 'std_types': list(std_types) | |
| } | |
| # Header parsing | |
| match = re.search(r"/\*\*?(.*?)\*/", source, re.DOTALL) | |
| if match: | |
| header = match.group(1).strip() | |
| outp['header_comment'] = header | |
| if 'Copyright' in header: | |
| copyright_match = re.search(r'Copyright\s*:\s*(.*?)(?:\n|$)', header, re.I) | |
| if copyright_match: | |
| outp['license'] = copyright_match.group(1).strip() | |
| if 'Authors' in header: | |
| authors_match = re.search(r'Authors\s*:\s*(.*?)(?:\n|$)', header, re.I) | |
| if authors_match: | |
| outp['authors'] = authors_match.group(1).strip() | |
| def traverse_nodes(nodes: List[Dict], namespace: Optional[str] = None): | |
| for node in nodes: | |
| if node.get('kind') == 'NamespaceDecl': | |
| ns_name = node.get('name', '') | |
| if ns_name == 'std' or ns_name.startswith('std::') or (namespace and namespace.startswith('std')): | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Skipped namespace {ns_name} due to std prefix\n") | |
| continue | |
| new_ns = f'{namespace}.{ns_name}' if namespace else ns_name | |
| traverse_nodes(node.get('inner', []), new_ns) | |
| elif node.get('kind') in ['FunctionDecl', 'CXXMethodDecl', 'CXXConstructorDecl', 'CXXDestructorDecl']: | |
| decl = process_function(node, namespace, source, stderr_log, timestamp) | |
| if decl: | |
| outp['decls'].append(decl) | |
| elif node.get('kind') == 'CXXRecordDecl': | |
| if node.get('fieldName'): | |
| continue | |
| decl = process_record(node, namespace, source, stderr_log, timestamp) | |
| if decl: | |
| outp['decls'].append(decl) | |
| elif node.get('kind') == 'FieldDecl' and 'type' in node and 'Record' in node.get('type', {}).get('qualType', ''): | |
| inner_record = next((inner for inner in node.get('inner', []) if inner.get('kind') == 'CXXRecordDecl'), None) | |
| if inner_record and not inner_record.get('name'): | |
| inner_record['fieldName'] = node.get('name', '') | |
| decl = process_record(inner_record, namespace, source, stderr_log, timestamp) | |
| if decl: | |
| outp['decls'].append(decl) | |
| elif node.get('kind') in ['TypedefDecl', 'TypeAliasDecl']: | |
| decl = process_typedef(node, namespace, source, stderr_log, timestamp) | |
| if decl: | |
| outp['decls'].append(decl) | |
| traverse_nodes(ast.get('inner', [])) | |
| with open(stderr_log, 'a') as f: | |
| f.write(f"[{timestamp}] Processed {len(outp['decls'])} declarations\n") | |
| if not outp['decls']: | |
| f.write(f"[{timestamp}] Warning: No declarations generated; check include paths or input file\n") | |
| with open(json_file, 'w') as f: | |
| json.dump(outp, f, indent=2) | |
| return outp | |
| def generate_d_code(ir: Dict) -> str: | |
| """Convert IR to D code.""" | |
| timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| # Header comments | |
| d_code = [] | |
| if ir['header_comment']: | |
| for line in ir['header_comment'].splitlines(): | |
| d_code.append(f"/// {line.strip()}") | |
| d_code.append(f"/// License: {ir['license']}" if ir['license'] else '// License: Unknown') | |
| d_code.append(f"/// Authors: {ir['authors']}" if ir['authors'] else '// Authors: Unknown') | |
| d_code.append('// Automatically generated D bindings') | |
| # Add imports only if needed | |
| std_imports = [] | |
| memory_needed = ir.get('uses_memory') and any(t in ir['std_types'] for t in ['std::allocator']) | |
| has_classes = any(decl['kind'] == 'class' for decl in ir['decls']) | |
| if memory_needed: | |
| std_imports.append('import core.stdcpp.memory;') | |
| if any(t in ir['std_types'] for t in ['std::string', 'std::basic_string']): | |
| std_imports.append('import core.stdcpp.string;') | |
| if any(t in ir['std_types'] for t in ['std::vector']): | |
| std_imports.append('import core.stdcpp.vector;') | |
| if any(t in ir['std_types'] for t in ['std::allocator']): | |
| std_imports.append('import core.stdcpp.allocator;') | |
| if any(t in ir['std_types'] for t in ['__int128_t', '__uint128_t']): | |
| std_imports.append('import core.int128;') | |
| if has_classes: | |
| std_imports.append('import core.stdcpp.new_;') | |
| if std_imports: | |
| d_code.extend(std_imports) | |
| d_code.append('') | |
| # Add 128-bit integer aliases if needed | |
| if any(t in ir['std_types'] for t in ['__int128_t', '__uint128_t']): | |
| d_code.append('alias int128_t = cent;') | |
| d_code.append('alias uint128_t = ucent;') | |
| d_code.append('') | |
| namespace_decls = {} | |
| def add_decl(decl: Dict, ns_key: str): | |
| if ns_key not in namespace_decls: | |
| namespace_decls[ns_key] = [] | |
| namespace_decls[ns_key].append(decl) | |
| for decl in ir['decls']: | |
| ns_key = decl.get('namespace', '') | |
| if decl['kind'] == 'alias': | |
| code = [] | |
| if 'comment' in decl: | |
| comment_lines = decl['comment'].strip().splitlines() | |
| code.append('/++') | |
| for line in comment_lines: | |
| code.append(f" + {line.strip().replace('/*', '').replace('*/', '')}") | |
| code.append('+/') | |
| code.append(f"alias {decl['name']} = {decl['type']};") | |
| add_decl('\n'.join(code), ns_key) | |
| elif decl['kind'] in ['struct', 'class']: | |
| code = [] | |
| if decl.get('is_incomplete'): | |
| if 'comment' in decl: | |
| comment_lines = decl['comment'].strip().splitlines() | |
| code.append('/++') | |
| for line in comment_lines: | |
| code.append(f" + {line.strip().replace('/*', '').replace('*/', '')}") | |
| code.append('+/') | |
| code.append(f"/// Warning: Incomplete definition") | |
| code.append(f"extern(C++, {decl['kind']}) struct {decl['name']};") | |
| else: | |
| if 'comment' in decl: | |
| comment_lines = decl['comment'].strip().splitlines() | |
| code.append('/++') | |
| for line in comment_lines: | |
| code.append(f" + {line.strip().replace('/*', '').replace('*/', '')}") | |
| code.append('+/') | |
| if decl['kind'] == 'class': | |
| code.append(f"/// Note: Instantiate with `new {decl['name']}(...)` or `core.stdcpp.new_.cpp_new!{decl['name']}(...)`") | |
| code.append(f"extern(C++, {decl['kind']}) class {decl['name']} {{") | |
| for field in decl['fields']: | |
| if field.get('comment'): | |
| comment_lines = field['comment'].strip().splitlines() | |
| code.append(' /++') | |
| for line in comment_lines: | |
| code.append(f" + {line.strip().replace('/*', '').replace('*/', '')}") | |
| code.append(' +/') | |
| code.append(f" {field['type']} {field['name']};") | |
| for alias in decl['aliases']: | |
| if 'comment' in alias: | |
| comment_lines = alias['comment'].strip().splitlines() | |
| code.append(' /++') | |
| for line in comment_lines: | |
| code.append(f" + {line.strip().replace('/*', '').replace('*/', '')}") | |
| code.append(' +/') | |
| code.append(f" alias {alias['name']} = {alias['type']};") | |
| for method in decl['methods']: | |
| method_code = [] | |
| if 'comment' in method: | |
| comment_lines = method['comment'].strip().splitlines() | |
| method_code.append(' /++') | |
| for line in comment_lines: | |
| method_code.append(f" + {line.strip().replace('/*', '').replace('*/', '')}") | |
| method_code.append(' +/') | |
| if method.get('mangled'): | |
| method_code.append(f" pragma(mangle, \"{method['mangled']}\")") | |
| attrs = ' '.join(method['attributes']) + (' ' if method['attributes'] else '') | |
| if method['kind'] == 'constructor': | |
| # Skip constructors for structs | |
| if decl['kind'] == 'struct': | |
| with open('clang_stderr.log', 'a') as f: | |
| f.write(f"[{timestamp}] Skipped constructor for struct {decl['name']} (not allowed in D)\n") | |
| continue | |
| method_code.append(f" {attrs}this({', '.join(f'{p['type']} {p['name']}' for p in method['params'])});") | |
| elif method['kind'] == 'destructor': | |
| method_code.append(f" {attrs}~this();") | |
| else: | |
| method_code.append(f" {attrs}{method['return']} {method['name']}({', '.join(f'{p['type']} {p['name']}' for p in method['params'])});") | |
| code.extend(method_code) | |
| code.append('}') | |
| add_decl('\n'.join(code), ns_key) | |
| elif decl['kind'] == 'func': | |
| code = [] | |
| if 'comment' in decl: | |
| comment_lines = decl['comment'].strip().splitlines() | |
| code.append('/++') | |
| for line in comment_lines: | |
| code.append(f" + {line.strip().replace('/*', '').replace('*/', '')}") | |
| code.append('+/') | |
| if decl.get('mangled'): | |
| code.append(f"pragma(mangle, \"{decl['mangled']}\")") | |
| attrs = ' '.join(decl['attributes']) + (' ' if decl['attributes'] else '') | |
| code.append(f"{attrs}{decl['return']} {decl['name']}({', '.join(f'{p['type']} {p['name']}' for p in decl['params'])});") | |
| add_decl('\n'.join(code), ns_key) | |
| for ns_key, decls in namespace_decls.items(): | |
| if ns_key: | |
| d_code.append(f'extern(C++, {ns_key}) {{') | |
| d_code.extend(decls) | |
| d_code.append('}') | |
| else: | |
| d_code.extend(decls) | |
| return '\n'.join(d_code) | |
| def main(): | |
| parser = argparse.ArgumentParser(description='Generate D bindings from C++ source file.') | |
| parser.add_argument('cpp_source_file', help='Path to the C++ source file') | |
| parser.add_argument('-I', '--include', action='append', default=[], help='Additional include paths for Clang') | |
| parser.add_argument('-o', '--output', help='Output D file path (default: <input>.d)') | |
| parser.add_argument('--stdout-log', default='clang_stdout.log', help='Log file for Clang stdout (default: clang_stdout.log)') | |
| parser.add_argument('--stderr-log', default='clang_stderr.log', help='Log file for Clang stderr (default: clang_stderr.log)') | |
| parser.add_argument('--json-file', default='bindings_ir.json', help='Output file for IR JSON (default: bindings_ir.json)') | |
| args = parser.parse_args() | |
| csrc_path = args.cpp_source_file | |
| include_paths = args.include | |
| output_path = args.output or csrc_path.replace('.cpp', '.d').replace('.hpp', '.d') | |
| stdout_log = args.stdout_log | |
| stderr_log = args.stderr_log | |
| json_file = args.json_file | |
| with open(csrc_path, mode='r', newline='') as f: | |
| source = f.read() | |
| ir = generate_ir(csrc_path, include_paths, stdout_log, stderr_log, json_file, source) | |
| d_bindings = generate_d_code(ir) | |
| try: | |
| with open(output_path, 'w') as f: | |
| f.write(d_bindings) | |
| print(f"D bindings written to {output_path}") | |
| except IOError as e: | |
| print(f"Error writing to {output_path}: {e}") | |
| sys.exit(1) | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment