[autofit] Enable dynamic loading of HarfBuzz. (1/2)

This commit activates the mini-HarfBuzz header files and provides the
necessary infrastructure for dynamically loading HarfBuzz if
`FT_CONFIG_OPTION_USE_HARFBUZZ_DYNAMIC` is defined (this macro gets set up
in a follow-up commit).

* src/autofit/ft-hb.c: New file, providing `ft_hb_funcs_init` and
  `ft_hb_funcs_done` for loading HarfBuzz dynamically.  The name of the
  library is hold in the macro `FT_LIBHARFBUZZ`, which can be overridden.

* src/autofit/ft-hb.h: Don't include `hb.h` but `ft-hb-types.h`.
  (hb): Modified to handle both standard linking and dynamically
  loading of HarfBuzz.
  (HB_EXTERN): New macro to load `ft-hb-decls.h`.

* src/autofit/afadjust.c [FT_CONFIG_OPTION_USE_HARFBUZZ]: For the sake of
  dynamically loading the HarfBuzz library, replace the compile-time macro
  `HB_VERSION_ATLEAST` with a call to the run-time function
  `hb_version_atleast` where necessary – a follow-up commit will set the
  minimum version of HarfBuzz to 2.6.8, which provides all necessary
  functions needed by FreeType.

* src/autofit/afmodule.h: Include `ft-hb.h`.
  (AF_ModuleRec) [FT_CONFIG_OPTION_USE_HARFBUZZ_DYNAMIC]: Add `hb_funcs`
  structure to hold pointers to the dynamically loaded HarfBuzz functions.

* src/autofit/afmodule.c (af_autofitter_init, af_autofitter_done)
  [FT_CONFIG_OPTION_USE_HARFBUZZ_DYNAMIC]: Call `ft_hb_funcs_init` and
  `ft_hb_funcs_done`.

* src/autofit/afshaper.h: Updated.
* src/autofit/autofit.c: Include `ft-hb.c`.

* src/autofit/rules.mk (AUTOF_DRV_SRC, AUTOF_DRV_H): Updated.
diff --git a/src/autofit/afadjust.c b/src/autofit/afadjust.c
index 6e9f5f7..9e1b807 100644
--- a/src/autofit/afadjust.c
+++ b/src/autofit/afadjust.c
@@ -987,7 +987,6 @@
 
 
 #ifdef FT_CONFIG_OPTION_USE_HARFBUZZ
-#  if HB_VERSION_ATLEAST( 7, 2, 0 )
 
   /*
     Find all glyphs that a code point could turn into from the OpenType
@@ -1124,7 +1123,6 @@
     hb( set_destroy )( helper_result );
   }
 
-#  endif /* HB_VERSION_ATLEAST */
 #endif /* FT_CONFIG_OPTION_USE_HARFBUZZ */
 
 
@@ -1166,8 +1164,8 @@
       goto Exit;
 
 #ifdef FT_CONFIG_OPTION_USE_HARFBUZZ
-#  if HB_VERSION_ATLEAST( 7, 2, 0 )
 
+    if ( hb( version_atleast )( 7, 2, 0 ) )
     {
       /* No need to check whether HarfBuzz has allocation issues; */
       /* it continues to work in such cases and simply returns    */
@@ -1277,10 +1275,9 @@
         codepoint = FT_Get_Next_Char( face, codepoint, &glyph_index );
       }
     }
+    else
 
-#  endif /* HB_VERSION_ATLEAST */
-
-#else /* !FT_CONFIG_OPTION_USE_HARFBUZZ */
+#endif /* FT_CONFIG_OPTION_USE_HARFBUZZ */
 
     {
       FT_UInt  i;
@@ -1313,8 +1310,6 @@
                 af_reverse_character_map_entry_compare );
     }
 
-#endif /* !FT_CONFIG_OPTION_USE_HARFBUZZ */
-
     FT_TRACE4(( "    reverse character map built successfully"
                 " with %ld entries\n", ( *map )->length ));
 
diff --git a/src/autofit/afmodule.c b/src/autofit/afmodule.c
index 726f6ca..9c1f2c2 100644
--- a/src/autofit/afmodule.c
+++ b/src/autofit/afmodule.c
@@ -412,6 +412,11 @@
     module->darken_params[6]  = CFF_CONFIG_OPTION_DARKENING_PARAMETER_X4;
     module->darken_params[7]  = CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y4;
 
+#if defined( FT_CONFIG_OPTION_USE_HARFBUZZ )         && \
+    defined( FT_CONFIG_OPTION_USE_HARFBUZZ_DYNAMIC )
+    ft_hb_funcs_init( module );
+#endif
+
     return FT_Err_Ok;
   }
 
@@ -421,6 +426,11 @@
   {
     FT_UNUSED( ft_module );
 
+#if defined( FT_CONFIG_OPTION_USE_HARFBUZZ )         && \
+    defined( FT_CONFIG_OPTION_USE_HARFBUZZ_DYNAMIC )
+    ft_hb_funcs_done( (AF_Module)ft_module );
+#endif
+
 #ifdef FT_DEBUG_AUTOFIT
     if ( af_debug_hints_rec_->memory )
       af_glyph_hints_done( af_debug_hints_rec_ );
diff --git a/src/autofit/afmodule.h b/src/autofit/afmodule.h
index 91a1abf..f5a406c 100644
--- a/src/autofit/afmodule.h
+++ b/src/autofit/afmodule.h
@@ -22,6 +22,7 @@
 #include <freetype/internal/ftobjs.h>
 #include <freetype/ftmodapi.h>
 
+#include "ft-hb.h"
 
 FT_BEGIN_HEADER
 
@@ -40,6 +41,11 @@
     FT_Bool       no_stem_darkening;
     FT_Int        darken_params[8];
 
+#if defined( FT_CONFIG_OPTION_USE_HARFBUZZ )         && \
+    defined( FT_CONFIG_OPTION_USE_HARFBUZZ_DYNAMIC )
+    ft_hb_funcs_t*  hb_funcs;
+#endif
+
   } AF_ModuleRec, *AF_Module;
 
 
diff --git a/src/autofit/afshaper.h b/src/autofit/afshaper.h
index f729953..4a4176f 100644
--- a/src/autofit/afshaper.h
+++ b/src/autofit/afshaper.h
@@ -23,15 +23,6 @@
 #include <freetype/freetype.h>
 
 
-#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ
-
-#include <hb.h>
-#include <hb-ot.h>
-#include "ft-hb-ft.h"
-
-#endif
-
-
 FT_BEGIN_HEADER
 
   FT_Error
diff --git a/src/autofit/autofit.c b/src/autofit/autofit.c
index a283e69..e7da68c 100644
--- a/src/autofit/autofit.c
+++ b/src/autofit/autofit.c
@@ -18,6 +18,7 @@
 
 #define FT_MAKE_OPTION_SINGLE_OBJECT
 
+#include "ft-hb.c"
 #include "ft-hb-ft.c"
 #include "afadjust.c"
 #include "afblue.c"
diff --git a/src/autofit/ft-hb.c b/src/autofit/ft-hb.c
new file mode 100644
index 0000000..110d7bf
--- /dev/null
+++ b/src/autofit/ft-hb.c
@@ -0,0 +1,165 @@
+/****************************************************************************
+ *
+ * ft-hb.c
+ *
+ *   FreeType-HarfBuzz bridge (body).
+ *
+ * Copyright (C) 2025 by
+ * Behdad Esfahbod.
+ *
+ * This file is part of the FreeType project, and may only be used,
+ * modified, and distributed under the terms of the FreeType project
+ * license, LICENSE.TXT.  By continuing to use, modify, or distribute
+ * this file you indicate that you have read the license and
+ * understand and accept it fully.
+ *
+ */
+
+
+#include <freetype/freetype.h>
+#include <freetype/internal/ftmemory.h>
+
+#include "afglobal.h"
+
+#include "ft-hb.h"
+
+
+#if defined( FT_CONFIG_OPTION_USE_HARFBUZZ )         && \
+    defined( FT_CONFIG_OPTION_USE_HARFBUZZ_DYNAMIC )
+
+#ifndef FT_LIBHARFBUZZ
+#  ifdef _WIN32
+#    define FT_LIBHARFBUZZ "libharfbuzz-0.dll"
+#  else
+#    ifdef __APPLE__
+#      define FT_LIBHARFBUZZ "libharfbuzz.0.dylib"
+#    else
+#      define FT_LIBHARFBUZZ "libharfbuzz.so.0"
+#    endif
+#  endif
+#endif
+
+#ifdef _WIN32
+
+#  include <windows.h>
+
+#else /* !_WIN32 */
+
+#  include <dlfcn.h>
+
+  /* The GCC pragma suppresses the warning "ISO C forbids     */
+  /* assignment between function pointer and 'void *'", which */
+  /* inevitably gets emitted with `-Wpedantic`; see the man   */
+  /* page of function `dlsym` for more information.           */
+#  if defined( __GNUC__ )
+#    pragma GCC diagnostic push
+#    ifndef __cplusplus
+#      pragma GCC diagnostic ignored "-Wpedantic"
+#    endif
+#  endif
+
+#endif /* !_WIN32 */
+
+
+  FT_LOCAL_DEF( void )
+  ft_hb_funcs_init( struct AF_ModuleRec_  *af_module )
+  {
+    FT_Memory  memory = af_module->root.memory;
+    FT_Error   error;
+
+    ft_hb_funcs_t                *funcs           = NULL;
+    ft_hb_version_atleast_func_t  version_atleast = NULL;
+
+#ifdef _WIN32
+    HANDLE  lib;
+#  define DLSYM( lib, name ) \
+            (ft_ ## name ## _func_t)GetProcAddress( lib, #name )
+#else
+    void  *lib;
+#  define DLSYM( lib, name ) \
+            (ft_ ## name ## _func_t)dlsym( lib, #name )
+#endif
+
+
+    af_module->hb_funcs = NULL;
+
+    if ( FT_NEW( funcs ) )
+      return;
+    FT_ZERO( funcs );
+
+#ifdef _WIN32
+
+    lib = LoadLibraryA( FT_LIBHARFBUZZ );
+    if ( !lib )
+      goto Fail;
+    version_atleast = DLSYM( lib, hb_version_atleast );
+
+#else /* !_WIN32 */
+
+    lib = RTLD_DEFAULT;
+    version_atleast = DLSYM( lib, hb_version_atleast );
+    if ( !version_atleast )
+    {
+      /* Load the HarfBuzz library.
+       *
+       * We never close the library, since we opened it with RTLD_GLOBAL.
+       * This is important for the case where we are using HarfBuzz as a
+       * shared library, and we want to use the symbols from the library in
+       * other shared libraries or clients.  HarfBuzz holds onto global
+       * variables, and closing the library will cause them to be
+       * invalidated.
+       */
+      lib = dlopen( FT_LIBHARFBUZZ, RTLD_LAZY | RTLD_GLOBAL );
+      if ( !lib )
+        goto Fail;
+      version_atleast = DLSYM( lib, hb_version_atleast );
+      if ( !version_atleast )
+        goto Fail;
+    }
+
+#endif /* !_WIN32 */
+
+    /* Load all symbols we use. */
+#define HB_EXTERN( ret, name, args )  \
+  {                                   \
+    funcs->name = DLSYM( lib, name ); \
+    if ( !funcs->name )               \
+      goto Fail;                      \
+  }
+#include "ft-hb-decls.h"
+#undef HB_EXTERN
+
+#undef DLSYM
+
+    af_module->hb_funcs = funcs;
+    return;
+
+  Fail:
+    if ( funcs )
+      FT_FREE( funcs );
+  }
+
+
+  FT_LOCAL_DEF( void )
+  ft_hb_funcs_done( struct AF_ModuleRec_  *af_module )
+  {
+    FT_Memory  memory = af_module->root.memory;
+
+
+    if ( af_module->hb_funcs )
+    {
+      FT_FREE( af_module->hb_funcs );
+      af_module->hb_funcs = NULL;
+    }
+  }
+
+#ifndef _WIN32
+#  if defined( __GNUC__ )
+#    pragma GCC diagnostic pop
+#  endif
+#endif
+
+#endif /* !FT_CONFIG_OPTION_USE_HARFBUZZ_DYNAMIC */
+
+
+/* END */
diff --git a/src/autofit/ft-hb.h b/src/autofit/ft-hb.h
index 05e8df5..399bfa9 100644
--- a/src/autofit/ft-hb.h
+++ b/src/autofit/ft-hb.h
@@ -19,8 +19,6 @@
 #ifndef FT_HB_H
 #define FT_HB_H
 
-#include <hb.h>
-
 #include <freetype/internal/compiler-macros.h>
 #include <freetype/freetype.h>
 
@@ -29,10 +27,47 @@
 
 #ifdef FT_CONFIG_OPTION_USE_HARFBUZZ
 
-#define hb( x )  hb_ ## x
+#  include "ft-hb-types.h"
+
+#  ifdef FT_CONFIG_OPTION_USE_HARFBUZZ_DYNAMIC
+
+#    define HB_EXTERN( ret, name, args ) \
+              typedef ret (*ft_ ## name ## _func_t) args;
+#    include "ft-hb-decls.h"
+#    undef HB_EXTERN
+
+  typedef struct ft_hb_funcs_t
+  {
+#    define HB_EXTERN( ret, name, args ) \
+              ft_ ## name ## _func_t  name;
+#    include "ft-hb-decls.h"
+#    undef HB_EXTERN
+  } ft_hb_funcs_t;
+
+  struct  AF_ModuleRec_;
+
+  FT_LOCAL( void )
+  ft_hb_funcs_init( struct AF_ModuleRec_  *af_module );
+
+  FT_LOCAL( void )
+  ft_hb_funcs_done( struct AF_ModuleRec_  *af_module );
+
+#    define hb( x )  globals->module->hb_funcs->hb_ ## x
+
+#  else /* !FT_CONFIG_OPTION_USE_HARFBUZZ_DYNAMIC */
+
+#    define HB_EXTERN( ret, name, args ) \
+              ret name args;
+#    include "ft-hb-decls.h"
+#    undef HB_EXTERN
+
+#    define hb( x )  hb_ ## x
+
+#  endif /* !FT_CONFIG_OPTION_USE_HARFBUZZ_DYNAMIC */
 
 #endif /* FT_CONFIG_OPTION_USE_HARFBUZZ */
 
+
 FT_END_HEADER
 
 #endif /* FT_HB_H */
diff --git a/src/autofit/rules.mk b/src/autofit/rules.mk
index b8766d3..c10288f 100644
--- a/src/autofit/rules.mk
+++ b/src/autofit/rules.mk
@@ -40,18 +40,20 @@
                  $(AUTOF_DIR)/afmodule.c \
                  $(AUTOF_DIR)/afranges.c \
                  $(AUTOF_DIR)/afshaper.c \
+                 $(AUTOF_DIR)/ft-hb.c    \
                  $(AUTOF_DIR)/ft-hb-ft.c
 
 # AUTOF driver headers
 #
-AUTOF_DRV_H := $(AUTOF_DRV_SRC:%c=%h)   \
-               $(AUTOF_DIR)/afcover.h   \
-               $(AUTOF_DIR)/aferrors.h  \
-               $(AUTOF_DIR)/afscript.h  \
-               $(AUTOF_DIR)/afstyles.h  \
-               $(AUTOF_DIR)/aftypes.h   \
-               $(AUTOF_DIR)/afws-decl.h \
-               $(AUTOF_DIR)/afws-iter.h
+AUTOF_DRV_H := $(AUTOF_DRV_SRC:%c=%h)     \
+               $(AUTOF_DIR)/afcover.h     \
+               $(AUTOF_DIR)/aferrors.h    \
+               $(AUTOF_DIR)/afscript.h    \
+               $(AUTOF_DIR)/afstyles.h    \
+               $(AUTOF_DIR)/aftypes.h     \
+               $(AUTOF_DIR)/afws-decl.h   \
+               $(AUTOF_DIR)/afws-iter.h   \
+               $(AUTOF_DIR)/ft-hb-decls.h
 
 
 # AUTOF driver object(s)