diff --git a/.gitignore b/.gitignore index 8ac649d..03e56af 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,8 @@ venv # Mac OS .DS_store + +# Build temporaries +_*.o +_*.so +_*.c diff --git a/Makefile b/Makefile index e75fa59..dac3ee4 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,9 @@ clean: clean-generated-ffi clean-build clean-pyc clean-test ## remove all build, clean-generated-ffi: ## remove FFI generated artifacts rm -fr pangocffi/_generated/ + rm -f _pangocffi*.so + rm -f _pangocffi*.c + rm -f _pangocffi*.o clean-build: ## remove build artifacts rm -fr build/ diff --git a/pangocffi/__init__.py b/pangocffi/__init__.py index 200938b..02ff346 100644 --- a/pangocffi/__init__.py +++ b/pangocffi/__init__.py @@ -1,20 +1,40 @@ +import ctypes.util import os import warnings from typing import List -import ctypes.util -from .ffi_build import ffi + + +# Attempt api mode, then precompiled abi mode, then import time abi +cffi_mode = "(unknown)" +try: + # Note in ABI mode lib is already available, no dlopen() needed + from ._pangocffi import ffi, lib as pango + gobject = pango + glib = pango + cffi_mode = "api" +except ImportError: + try: + # Note in ABI mode lib will be missing + from ._pangocffi import ffi + cffi_mode = "abi_precompiled" + except ImportError: + # Fall back to importing and parsing cffi defs + from .ffi_build import ffi_for_mode + ffi = ffi_for_mode("abi") + cffi_mode = "abi" def _dlopen(dl_name: str, generated_ffi, names: List[str]): """ :param dl_name: - The name of the dynamic library. This is also used to determine the - environment variable name to lookup. For example, if dl_name is "glib", - this function will attempt to load "GLIB_LOCATION". + The name of the dynamic library. This is also used to determine + the environment variable name to lookup. For example, if dl_name + is "glib", this function will attempt to load "GLIB_LOCATION". :param generated_ffi: The FFI for pango/gobject/glib, generated by pangocffi. :param names: - An array of library names commonly used across different platforms. + An array of library names commonly used across different + platforms. :return: A FFILibrary instance for the library. """ @@ -44,9 +64,13 @@ def _dlopen(dl_name: str, generated_ffi, names: List[str]): ) -pango = _dlopen('pango', ffi, ['pango', 'pango-1', 'pango-1.0', 'pango-1.0-0']) -gobject = _dlopen('gobject', ffi, ['gobject-2.0', 'gobject-2.0-0']) -glib = _dlopen('glib', ffi, ['glib-2.0', 'glib-2.0-0']) +# Fall back to non api mode +if cffi_mode != "api": + pango = _dlopen('pango', ffi, ['pango', 'pango-1', 'pango-1.0', + 'pango-1.0-0']) + gobject = _dlopen('gobject', ffi, ['gobject-2.0', 'gobject-2.0-0']) + glib = _dlopen('glib', ffi, ['glib-2.0', 'glib-2.0-0']) + # Imports are normally always put at the top of the file. # But the wrapper API requires that the pango library be loaded first. diff --git a/pangocffi/attribute.py b/pangocffi/attribute.py index c157726..c232c18 100644 --- a/pangocffi/attribute.py +++ b/pangocffi/attribute.py @@ -1,5 +1,6 @@ from . import FontDescription, Underline, ffi, pango, PangoObject -from .enums import Gravity, GravityHint, Stretch, Style, Variant, Weight +from .enums import (AttrIndex, Gravity, GravityHint, Stretch, Style, + Variant, Weight) from .rectangle import Rectangle @@ -20,9 +21,9 @@ def _get_start_index(self): return self._pointer.start_index def _set_start_index(self, start_index: int): - assert start_index >= pango.PANGO_ATTR_INDEX_FROM_TEXT_BEGINNING, \ + assert start_index >= AttrIndex.PANGO_ATTR_INDEX_FROM_TEXT_BEGINNING, \ "start_index is too low" - assert start_index < pango.PANGO_ATTR_INDEX_TO_TEXT_END, \ + assert start_index < AttrIndex.PANGO_ATTR_INDEX_TO_TEXT_END, \ "start_index is too high" start = ffi.cast("guint", start_index) self._pointer.start_index = start @@ -35,7 +36,7 @@ def _get_end_index(self): def _set_end_index(self, end_index: int): assert isinstance(end_index, int), "end_index isn't a int" - assert end_index <= pango.PANGO_ATTR_INDEX_TO_TEXT_END, \ + assert end_index <= AttrIndex.PANGO_ATTR_INDEX_TO_TEXT_END, \ "end_index is too high" end = ffi.cast("guint", end_index) self._pointer.end_index = end diff --git a/pangocffi/c_definitions_glib.txt b/pangocffi/c_definitions_glib.txt index 31ee243..9c194fe 100644 --- a/pangocffi/c_definitions_glib.txt +++ b/pangocffi/c_definitions_glib.txt @@ -1,4 +1,3 @@ -typedef ... GType; typedef unsigned int guint8; typedef unsigned int guint16; typedef unsigned int guint32; @@ -10,8 +9,8 @@ typedef char gchar; typedef unsigned char guchar; typedef guint32 gunichar; typedef void* gpointer; -typedef ... gconstpointer; -typedef ... GObject; +typedef const void* gconstpointer; +typedef void* GObject; typedef ... GObjectClass; typedef ... GString; typedef ... GDestroyNotify; diff --git a/pangocffi/c_definitions_pango.txt b/pangocffi/c_definitions_pango.txt index 7f60054..5e610f4 100644 --- a/pangocffi/c_definitions_pango.txt +++ b/pangocffi/c_definitions_pango.txt @@ -1,11 +1,7 @@ -typedef enum { - PANGO_ATTR_INDEX_FROM_TEXT_BEGINNING = 0, - PANGO_ATTR_INDEX_TO_TEXT_END = 4294967295 -} PangoAttrIndex; typedef ... hb_feature_t; typedef ... hb_font_t; typedef ... GBytes; -typedef ... GQuark; +typedef guint32 GQuark; typedef ... PangoAttrClass; typedef ... PangoAttrString; typedef ... PangoAttrLanguage; @@ -40,7 +36,6 @@ typedef ... PangoGlyphGeometry; typedef ... PangoGlyphVisAttr; typedef ... PangoGlyphInfo; typedef ... PangoGlyphString; -typedef ... PangoAnalysis; typedef ... PangoLayout; typedef ... PangoLayoutClass; typedef ... PangoLayoutLine; @@ -66,11 +61,23 @@ typedef struct int height; } PangoRectangle; typedef struct +{ + PangoEngineShape *shape_engine; + PangoEngineLang *lang_engine; + PangoFont *font; + guint8 level; + guint8 gravity; + guint8 flags; + guint8 script; + PangoLanguage *language; + GSList *extra_attrs; +} PangoAnalysis; +typedef struct { int offset; int length; int num_chars; - void * analysis; + PangoAnalysis analysis; } PangoItem; typedef struct { @@ -661,11 +668,6 @@ PangoCoverage *pango_coverage_from_bytes (guchar *bytes, GType pango_engine_get_type (void) ; GType pango_engine_lang_get_type (void) ; GType pango_engine_shape_get_type (void) ; -void script_engine_list (PangoEngineInfo **engines, - int *n_engines); -void script_engine_init (GTypeModule *module); -void script_engine_exit (void); -PangoEngine *script_engine_create (const char *id); GType pango_font_description_get_type (void) ; PangoFontDescription *pango_font_description_new (void); PangoFontDescription *pango_font_description_copy (const PangoFontDescription *desc); diff --git a/pangocffi/enums.py b/pangocffi/enums.py index fcb7280..ce78d70 100644 --- a/pangocffi/enums.py +++ b/pangocffi/enums.py @@ -1,5 +1,6 @@ +from enum import Enum, IntEnum + from pangocffi import pango -from enum import Enum class Style(Enum): @@ -270,3 +271,20 @@ class TabAlign(Enum): appears to the left of the tab stop position (until the available space is filled), the rest to the right. Since: 1.50 """ + + +class AttrIndex(IntEnum): + """ + :class:`AttrIndex` is used for specifying relative indexing of + attributes. + """ + + PANGO_ATTR_INDEX_FROM_TEXT_BEGINNING = 0 + """ + Index from the begining of the text. + """ + + PANGO_ATTR_INDEX_TO_TEXT_END = 4294967295 + """ + Index to the end of the text. + """ diff --git a/pangocffi/ffi_build.py b/pangocffi/ffi_build.py index ee498ff..33d8309 100644 --- a/pangocffi/ffi_build.py +++ b/pangocffi/ffi_build.py @@ -6,29 +6,103 @@ """ +import platform +import shutil import sys +import tempfile from pathlib import Path -import importlib.util +from warnings import warn + +from cffi import FFI +from cffi.error import PkgConfigError, VerificationError +from setuptools.errors import CCompilerError, ExecError, PlatformError + sys.path.append(str(Path(__file__).parent)) -# Create an empty _generated folder if needed -(Path(__file__).parent / '_generated').mkdir(exist_ok=True) -# Because we can't directly load the instance builder (it would run -# ``__init__.py`` for any module import) we have to do this dubious import. -spec = importlib.util.spec_from_file_location( - 'ffi_instance_builder', - str(Path(__file__).parent / 'ffi_instance_builder.py') -) -ffi_instance_builder = importlib.util.module_from_spec(spec) -spec.loader.exec_module(ffi_instance_builder) +def ffi_for_mode(mode): + # Read the C definitions + c_definitions_glib_file = open( + str(Path(__file__).parent / 'c_definitions_glib.txt'), + 'r' + ) + c_definitions_pango_file = open( + str(Path(__file__).parent / 'c_definitions_pango.txt'), + 'r' + ) + c_definitions_glib = c_definitions_glib_file.read() + c_definitions_pango = c_definitions_pango_file.read() + + ffi = FFI() + # Mirror the GType setup in gobject/gtypes.h + if ffi.sizeof('void*') > ffi.sizeof('long'): + ffi.cdef('typedef unsigned int* GType;') + else: + ffi.cdef('typedef unsigned long GType;') + ffi.cdef(c_definitions_glib) + ffi.cdef(c_definitions_pango) + if mode == "api": + ffi.set_source_pkgconfig( + "pangocffi._pangocffi", + ['pango', 'glib-2.0', 'pangoft2'] + + (['pangoxft'] if platform.system() == 'Linux' else []), + r""" + #include "glib.h" + #include "glib-object.h" + #include "pango/pango.h" + #include "pango/pango-fontmap.h" + + #include + #if PANGO_VERSION < G_ENCODE_VERSION(1, 54) + int pango_item_get_char_offset ( + PangoItem* item) { + fprintf(stderr, "Unimplemented!!\n"); + return -1; + } + #endif + #if PANGO_VERSION < G_ENCODE_VERSION(1, 52) + PangoFont* + pango_font_map_reload_font ( + PangoFontMap* fontmap, + PangoFont* font, + double scale, + PangoContext* context, + const char* variations) { + fprintf(stderr, "Unimplemented!!\n"); + return NULL; + } + + #endif + """, + sources=[] + ) + else: + ffi.set_source("pangocffi._pangocffi", None) + return ffi + + +def build_ffi(): + """ + This will be called from setup() to return an FFI + which it will compile - work out here which type is + possible and return it. + """ + try: + ffi_api = ffi_for_mode("api") + file = ffi_api.compile(verbose=True, tmpdir=tempfile.gettempdir()) + shutil.copy(file, "pangocffi") + return ffi_api + except (CCompilerError, ExecError, PlatformError, + PkgConfigError, VerificationError) as e: + warn("Falling back to precompiled python mode: {}".format(str(e))) + + ffi_abi = ffi_for_mode("abi") + file = ffi_abi.compile(verbose=True, tmpdir=tempfile.gettempdir()) + shutil.copy(file, "pangocffi") + return ffi_abi -# Generate the bindings -ffiBuilder = ffi_instance_builder.FFIInstanceBuilder( - source='pangocffi._generated.ffi' -) -ffi = ffiBuilder.generate() if __name__ == '__main__': + ffi = build_ffi() ffi.compile() diff --git a/pangocffi/ffi_instance_builder.py b/pangocffi/ffi_instance_builder.py deleted file mode 100644 index f7e060e..0000000 --- a/pangocffi/ffi_instance_builder.py +++ /dev/null @@ -1,37 +0,0 @@ -""" - pangocffi.ffi_instance_builder - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - generates an FFI for pangocffi -""" - -from pathlib import Path -from cffi import FFI -from typing import Optional - - -class FFIInstanceBuilder: - - def __init__(self, source: Optional[str] = None): - self.source = source - - def generate(self) -> FFI: - # Read the C definitions - c_definitions_glib_file = open( - str(Path(__file__).parent / 'c_definitions_glib.txt'), - 'r' - ) - c_definitions_pango_file = open( - str(Path(__file__).parent / 'c_definitions_pango.txt'), - 'r' - ) - c_definitions_glib = c_definitions_glib_file.read() - c_definitions_pango = c_definitions_pango_file.read() - - ffi = FFI() - ffi.cdef(c_definitions_glib) - ffi.cdef(c_definitions_pango) - if self.source is not None: - ffi.set_source(self.source, None) - - return ffi diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ad0679d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools >= 64"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index 5724bce..6f1f300 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,8 +31,11 @@ project_urls = packages = find: setup_requires = setuptools + cffi >= 1.1.0 + cairocffi >= 1.7.2 install_requires = cffi >= 1.1.0 + cairocffi >= 1.7.2 python_requires = >= 3.8 [options.package_data] diff --git a/setup.py b/setup.py index ffd9878..ca0ee1e 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,20 @@ +import os import sys -import setuptools + +from setuptools import setup + if sys.version_info.major < 3: raise RuntimeError( 'pangocffi does not support Python 2.x. Please use Python 3.' ) -setuptools.setup() +setup( + name='pangocffi', + use_scm_version=True, + install_requires=['cffi >= 1.1.0'], + setup_requires=['cffi >= 1.1.0'], + packages=['pangocffi'], + cffi_modules=['pangocffi/ffi_build.py:build_ffi'] +) + diff --git a/utils/make_c_definitions.py b/utils/make_c_definitions.py index 57bdf1d..2f25cfb 100644 --- a/utils/make_c_definitions.py +++ b/utils/make_c_definitions.py @@ -66,20 +66,11 @@ def remove_autoptr_cleanup_macros(source: str) -> str: return source -def add_attr_index_definitions(cdefs: str) -> str: - cdefs = 'typedef enum {\n' + \ - ' PANGO_ATTR_INDEX_FROM_TEXT_BEGINNING = 0,\n' + \ - ' PANGO_ATTR_INDEX_TO_TEXT_END = 4294967295\n' + \ - '} PangoAttrIndex;\n' + \ - cdefs - return cdefs - - def add_extra_typedefs(cdefs: str) -> str: cdefs = 'typedef ... hb_feature_t;\n' + \ 'typedef ... hb_font_t;\n' + \ 'typedef ... GBytes;\n' + \ - 'typedef ... GQuark;\n' + \ + 'typedef guint32 GQuark;\n' + \ cdefs return cdefs @@ -144,6 +135,13 @@ def get_struct_for_opaque_typedef( return public_struct_definition + '\n' +def remove_gi_scanner_preprocessor( + input: str +) -> str: + regex = r"#ifndef\s*__GI_SCANNER__([^#]*)#else([^#]*)#endif" + return re.sub(regex, '\\1', input, flags=re.MULTILINE) + + def remove_opaque_typedef(cdefs: str, opaque_typedef_name: str): return re.sub(r"typedef\s+\.\.\.\s+%s;" % opaque_typedef_name, '', cdefs) @@ -195,7 +193,6 @@ def generate(pango_git_dir): 'pango-context.h', 'pango-coverage.h', 'pango-direction.h', - 'pango-engine.h', 'pango-font.h', 'pango-fontmap.h', 'pango-fontset.h', @@ -240,6 +237,16 @@ def generate(pango_git_dir): ) typedefs_opaque = remove_opaque_typedef(typedefs_opaque, 'PangoRectangle') + typedefs_struct += remove_gi_scanner_preprocessor( + get_struct_for_opaque_typedef( + 'PangoAnalysis', + pango_git_dir, + 'pango-item.h', + '_PangoAnalysis' + ) + ) + typedefs_opaque = remove_opaque_typedef(typedefs_opaque, 'PangoAnalysis') + typedefs_struct += get_struct_for_opaque_typedef( 'PangoItem', pango_git_dir, @@ -290,9 +297,6 @@ def generate(pango_git_dir): # insert extra typedefs for hb and GBytes typedefs_opaque = add_extra_typedefs(typedefs_opaque) - # insert definitions for attr index - typedefs_opaque = add_attr_index_definitions(typedefs_opaque) - cdefs = typedefs_opaque +\ typedefs_struct +\ typedefs_enum +\ @@ -306,7 +310,7 @@ def generate(pango_git_dir): cdefs = re.sub(r'const PangoItem', 'PangoItem', cdefs) # Convert PangoAnalysis (util we make PangoAnalysis non-opaque) - cdefs = re.sub(r'PangoAnalysis analysis;', 'void * analysis;', cdefs) + # cdefs = re.sub(r'PangoAnalysis analysis;', 'void * analysis;', cdefs) cdefs = remove_multiple_blank_lines(cdefs) cdefs = remove_multiple_spaces(cdefs)