Skip to content

Use a script to build "Python_undef.h" #141332

@Locked-chess-official

Description

@Locked-chess-official

Feature or enhancement

Proposal:

#ifdef MS_WIN64
#if defined(_M_X64) || defined(_M_AMD64)
#if defined(__clang__)
#define COMPILER ("[Clang " __clang_version__ "] 64 bit (AMD64) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]")
#define PY_SUPPORT_TIER 0
#elif defined(__INTEL_COMPILER)
#define COMPILER ("[ICC v." _Py_STRINGIZE(__INTEL_COMPILER) " 64 bit (amd64) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]")
#define PY_SUPPORT_TIER 0
#else
#define COMPILER _Py_PASTE_VERSION("64 bit (AMD64)")
#define PY_SUPPORT_TIER 1
#endif /* __clang__ */
#define PYD_PLATFORM_TAG "win_amd64"
#elif defined(_M_ARM64)
#define COMPILER _Py_PASTE_VERSION("64 bit (ARM64)")
#define PY_SUPPORT_TIER 3
#define PYD_PLATFORM_TAG "win_arm64"
#else
#define COMPILER _Py_PASTE_VERSION("64 bit (Unknown)")
#define PY_SUPPORT_TIER 0
#endif
#endif /* MS_WIN64 */

This is the code in "PC/pyconfig.h", it defines the macro "COMPILER" and break the syntax in v8:

enum StateTag : uint16_t {
  JS,
  GC,
  PARSER,
  BYTECODE_COMPILER,
  COMPILER,
  OTHER,
  EXTERNAL,
  ATOMICS_WAIT,
  IDLE,
  LOGGING,
  IDLE_EXTERNAL,
};

Due to that it is unsure whether there are the other macros which don't start with "Py" or "_Py" won't be undefined (mainly in "pyconfig.h"), here is the script to create the file:

Details
import os
import re
import datetime

def is_valid_macro_name(macro_name):
    """
    Determine whether a macro name is valid using Python's standard library methods.
    
    Args:
        macro_name: The macro name to check.
        
    Returns:
        bool: True if it's a valid Python identifier, False otherwise.
    """
    # Empty string is invalid
    if not macro_name:
        return False
    
    # Use str.isidentifier() to check for valid identifier syntax
    return macro_name.isidentifier()

def extract_macro_name(line):
    """Extract the macro name from a #define line (handles spaces between # and define)."""
    line = line.strip()

    # Match '#', optional spaces, 'define', spaces, and the macro name
    match = re.match(r'^#\s*define\s+([A-Za-z_][A-Za-z0-9_]*)', line)
    if not match:
        return None
    
    candidate = match.group(1)
    
    # Validate with standard identifier rules
    if candidate and is_valid_macro_name(candidate):
        return candidate
    return None

def is_standard_python_macro(macro_name):
    """
    Check whether a macro follows Python's standard naming conventions.
    Rules: Starts with Py, PY, _Py, _PY, or ends with _H.
    """
    standard_prefixes = ('Py', 'PY', '_Py', '_PY')
    return macro_name.startswith(standard_prefixes) or macro_name.endswith('_H')

def generate_undef_code(macro_name):
    """Generate the code to undefine a macro."""
    return f"""#ifndef DONOTUNDEF_{macro_name}
#ifdef {macro_name}
#undef {macro_name}
#endif
#endif

"""

def generate_python_undef_header(pyconfig_path, output_path=None):
    """
    Generate the Python_undef.h header file.
    
    Args:
        pyconfig_path: Path to pyconfig.h
        output_path: Output file path, defaults to Python_undef.h in the current directory.
    """
    if output_path is None:
        output_path = 'Python_undef.h'
    
    # Read pyconfig.h
    try:
        with open(pyconfig_path, 'r', encoding='utf-8') as f:
            lines = f.readlines()
    except FileNotFoundError:
        print(f"Error: File not found {pyconfig_path}")
        return False
    except Exception as e:
        print(f"Error reading file: {e}")
        return False
    
    # Collect macros
    macros_to_undef = []
    all_macros = []
    invalid_macros = []
    
    print("Analyzing pyconfig.h...")
    
    for i, line in enumerate(lines, 1):
        macro_name = extract_macro_name(line)
        if macro_name:
            all_macros.append(macro_name)
            
            # New rule: any macro not starting with Py/PY/_Py/_PY and not ending with _H is considered non-standard
            if not is_standard_python_macro(macro_name):
                macros_to_undef.append(macro_name)
                print(f"Line {i:4d}: Found non-standard macro '{macro_name}'")
        else:
            # Check if line looks like a define but has invalid name
            line = line.strip()
            if line.startswith('#'):
                m = re.match(r'^#\s*define\s+(\S+)', line)
                if m:
                    candidate = m.group(1)
                    if candidate and not is_valid_macro_name(candidate):
                        invalid_macros.append((i, candidate))
    
    # Deduplicate and sort
    macros_to_undef = sorted(set(macros_to_undef))
    
    # Header section
    header = f"""/*
 * Python_undef.h - Automatically generated macro undefinition header
 * 
 * This file is automatically generated from {os.path.basename(pyconfig_path)}
 * Contains macros that may need to be undefined to avoid conflicts with other libraries.
 * 
 * WARNING: This is an automatically generated file. Do not edit manually.
 * 
 * Usage:
 *   #include <Python.h>
 *   #include <Python_undef.h>
 *   #include <other_library_headers.h>
 * 
 * To preserve specific macros, define before including this header:
 *   #define DONOTUNDEF_MACRO_NAME
 * 
 * Generation rules:
 *   - Macros starting with Py_, PY_, _Py, _PY are preserved (Python standard)
 *   - Macros ending with _H are preserved (header guards)
 *   - All other macros are undefined
 *   - Macro name validation uses Python's standard identifier checking
 * 
 * Generated from: {os.path.abspath(pyconfig_path)}
 * Generated at: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
 * Total valid macros found: {len(all_macros)}
 * Macros to undef: {len(macros_to_undef)}
 * Invalid macro names skipped: {len(invalid_macros)}
 */

#ifndef PYTHON_UNDEF_H
#define PYTHON_UNDEF_H

#ifndef Py_PYTHON_H
#  error "Python_undef.h must be included *after* Python.h"
#endif

/*
 * Platform Note:
 * - The COMPILER macro is primarily defined in pyconfig.h on Windows
 * - Other platforms define compiler info in Python/getcompiler.c
 * - This macro and others can conflict with libraries such as V8
 */

"""
    
    # Generate undef code sections
    undef_sections = []
    for macro_name in macros_to_undef:
        undef_sections.append(generate_undef_code(macro_name))
    
    # Footer
    footer = """#endif /* PYTHON_UNDEF_H */
"""
    
    # Write output
    try:
        with open(output_path, 'w', encoding='utf-8', newline='\n') as f:
            f.write(header)
            f.writelines(undef_sections)
            f.write(footer)
        
        print(f"\n{'='*60}")
        print(f"Successfully generated: {output_path}")
        print(f"{'='*60}")
        print("Summary:")
        print(f"  - Total valid macro definitions: {len(all_macros)}")
        print(f"  - Macros to undefine: {len(macros_to_undef)}")
        print(f"  - Preserved standard macros: {len(all_macros) - len(macros_to_undef)}")
        print(f"  - Invalid macro names skipped: {len(invalid_macros)}")
        
        if invalid_macros:
            print(f"\nSkipped invalid macro names:")
            for line_num, invalid_macro in invalid_macros[:10]:  # show only first 10
                print(f"  Line {line_num:4d}: '{invalid_macro}'")
            if len(invalid_macros) > 10:
                print(f"  ... and {len(invalid_macros) - 10} more")
        
        if macros_to_undef:
            print(f"\nMacros to undefine (first 50):")
            for i, macro in enumerate(macros_to_undef[:50], 1):
                print(f"  {i:3d}. {macro}")
            if len(macros_to_undef) > 50:
                print(f"  ... and {len(macros_to_undef) - 50} more")
        
        print(f"\nUsage Notes:")
        print(f"  1. Include this file before including other library headers.")
        print(f"  2. Use DONOTUNDEF_XXX to protect macros that must be kept.")
        print(f"  3. Regenerate this file whenever rebuilding Python.")
        
        return True
        
    except Exception as e:
        print(f"Error writing file: {e}")
        return False

def test_macro_validation():
    """Test the macro name validation function."""
    test_cases = [
        ("COMPILER", True, "Valid identifier"),
        ("Py_InitModule", True, "Python standard macro"),
        ("PYCONFIG_H", True, "Header guard macro"),
        ("123MACRO", False, "Starts with a digit"),
        ("MACRO-TEST", False, "Contains a hyphen"),
        ("MACRO.TEST", False, "Contains a dot"),
        ("if", True, "C/C++ keyword but valid macro name"),
        ("for", True, "C/C++ keyword but valid macro name"),
        ("_Private", True, "Starts with underscore"),
        ("__special__", True, "Double underscore name"),
        ("", False, "Empty string"),
        ("MAX_VALUE", True, "Valid identifier"),
        ("SIZEOF_INT", True, "Valid identifier"),
        ("HAVE_STDLIB", True, "Valid identifier"),
    ]
    
    print("Macro name validation tests:")
    print("-" * 50)
    for macro, expected, description in test_cases:
        result = is_valid_macro_name(macro)
        status = "✓" if result == expected else "✗"
        print(f"{status} {macro:15} -> {result:5} ({description})")

if __name__ == "__main__":
    test_macro_validation()
    
    print(f"\n{'='*60}")
    print("Note: Python keywords are not excluded since they are valid macro names in C/C++.")
    print(f"{'='*60}")
    
    pyconfig_path = "pyconfig.h"  # modify as needed
    
    if os.path.exists(pyconfig_path):
        success = generate_python_undef_header(pyconfig_path)
        
        if success:
            print(f"\n✅ Generation complete!")
            print(f"💡 Tip: Place Python_undef.h inside Python include search path.")
        else:
            print(f"\n❌ Generation failed!")
            
    else:
        print(f"File {pyconfig_path} not found.")
        print("Please update the pyconfig_path variable to the actual pyconfig.h path.")
        print("\nTypical paths on Windows:")
        print("  C:\\\\Python3x\\\\include\\\\pyconfig.h")
        print("\nTypical paths on Unix/Linux:")
        print("  /usr/include/python3.x/pyconfig.h")
        print("  /usr/local/include/python3.x/pyconfig.h")

The macros which were defined in "pyconfig.h" and not standard only take part in the python itself building. Users can safely undefined them:

#include <Python.h>
#include <Python_undef.h> // To udefine the macros that may cause SyntaxError
#include <other_header.h>

If users want to use the macro, they can define the macro to force preserve it:

#include <Python.h>
#define DONOTUNDEF_macro_name
#include <Python_undef.h> // If the macro "macro_name" exist, it will be preserved
#include <other_header.h>

The action that include this header file to undefine the macros should be explicit.

Has this already been discussed elsewhere?

https://discuss.python.org/t/pre-pep-use-a-header-file-that-can-undefine-the-macros-defined-by-python-h/104784

Links to previous discussion of this feature:

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-featureA feature request or enhancement

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions