Skip to content

Commit

Permalink
simplify api vs abi mode and auto pick
Browse files Browse the repository at this point in the history
  • Loading branch information
georgeharker committed Dec 9, 2024
1 parent 1751247 commit 9fb5c61
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 156 deletions.
97 changes: 53 additions & 44 deletions pangocffi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,65 +4,74 @@
from typing import List


# Attempt api mode if installed
api_mode = True
# Attempt api mode, then precompiled abi mode, then import time abi
cffi_mode = "(unknown)"
try:
from _pangocffi import ffi
from _pangocffi import lib as pango
# Note in ABI mode lib is already available, no dlopen() needed
from _pangocffi import ffi, lib as pango
gobject = pango
glib = pango
api_mode = True
cffi_mode = "api"
except ImportError:
api_mode = False
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"

# Fall back to non api mode
if not api_mode:
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".
:param generated_ffi:
The FFI for pango/gobject/glib, generated by pangocffi.
:param names:
An array of library names commonly used across different
platforms.
:return:
A FFILibrary instance for the library.
"""

# Try environment locations if set
env_location = os.getenv(f'{dl_name.upper()}_LOCATION')
if env_location:
names.append(env_location)
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".
:param generated_ffi:
The FFI for pango/gobject/glib, generated by pangocffi.
:param names:
An array of library names commonly used across different
platforms.
:return:
A FFILibrary instance for the library.
"""

# Try environment locations if set
env_location = os.getenv(f'{dl_name.upper()}_LOCATION')
if env_location:
names.append(env_location)
try:
return generated_ffi.dlopen(env_location)
except OSError:
warnings.warn(f"dlopen() failed to load {dl_name} library:"
f" '{env_location}'. Falling back.")

# Try various names for the same library, for different platforms.
for name in names:
for lib_name in (name, 'lib' + name):
try:
return generated_ffi.dlopen(env_location)
path = ctypes.util.find_library(lib_name)
lib = generated_ffi.dlopen(path or lib_name)
if lib:
return lib
except OSError:
warnings.warn(f"dlopen() failed to load {dl_name} library:"
f" '{env_location}'. Falling back.")
pass
raise OSError(
f"dlopen() failed to load {dl_name} library: {' / '.join(names)}"
)

# Try various names for the same library, for different platforms.
for name in names:
for lib_name in (name, 'lib' + name):
try:
path = ctypes.util.find_library(lib_name)
lib = generated_ffi.dlopen(path or lib_name)
if lib:
return lib
except OSError:
pass
raise OSError(
f"dlopen() failed to load {dl_name} library: {' / '.join(names)}"
)

from .ffi_build import ffi # noqa

# 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.
# Therefore, we have to disable linting rules for these lines.
Expand Down
112 changes: 86 additions & 26 deletions pangocffi/ffi_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,98 @@
"""

import importlib.util
import os
import platform
import sys
from distutils.errors import CCompilerError, DistutilsExecError, DistutilsPlatformError
from pathlib import Path
from warnings import warn

from cffi import FFI
from cffi.error import VerificationError

sys.path.append(str(Path(__file__).parent))

api_mode = False
if ('PANGOCFFI_API_MODE' in os.environ and
int(os.environ['PANGOCFFI_API_MODE']) == 1):
# Allow explicit disable of api_mode
api_mode = True

# Create an empty _generated folder if needed
if not api_mode:
(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)

# Generate the bindings
ffiBuilder = ffi_instance_builder.FFIInstanceBuilder(
source='pangocffi._generated.ffi' if not api_mode else None
)
ffi = ffiBuilder.generate()

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",
['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 <stdio.h>
#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", 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")
ffi_api.compile(verbose=True)
return ffi_api
except (CCompilerError, DistutilsExecError, DistutilsPlatformError,
VerificationError) as e:
warn("Falling back to precompiled python mode: {}".format(str(e)))

ffi_abi = ffi_for_mode("abi")
ffi_abi.compile(verbose=True)
return ffi_abi


if __name__ == '__main__':
ffi = build_ffi()
ffi.compile()
80 changes: 0 additions & 80 deletions pangocffi/ffi_instance_builder.py

This file was deleted.

7 changes: 1 addition & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,12 @@
'pangocffi does not support Python 2.x. Please use Python 3.'
)

api_mode = False
if ('PANGOCFFI_API_MODE' in os.environ and
int(os.environ['PANGOCFFI_API_MODE']) == 1):
api_mode = True

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:ffi'] if api_mode else []
cffi_modules=['pangocffi/ffi_build.py:build_ffi']
)

0 comments on commit 9fb5c61

Please sign in to comment.