unit CkDllLoader;

// This file is not generated.

{
  Dynamically loads the Chilkat bridge library at runtime.

  Supported platforms and default library names:

    Windows  32-bit : chilkat_c_bridge_win32.dll
    Windows  64-bit : chilkat_c_bridge_x64.dll
    Windows  arm64  : chilkat_c_bridge_arm64.dll  (FPC only -- see note below)
    Linux    x86    : libchilkat_c_bridge_x86.so
    Linux    x86_64 : libchilkat_c_bridge_x86_64.so
    Linux    arm    : libchilkat_c_bridge_arm.so
    Linux    arm64  : libchilkat_c_bridge_arm64.so
    macOS    univ : libchilkat_c_bridge.dylib

  Compatible with Lazarus/FPC (Windows, Linux, macOS) and Delphi XE2+ (Windows).

  Library search order (unless SetChilkatDllPath is called):
    1. Already loaded by this process (Windows only)
    2. Standard OS search (PATH / LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / system dirs)
    3. Same directory as the application executable (full path fallback)
}

interface

{ Call before creating any Chilkat object to specify a full path to the
  library. If not called, the default name above is used. }
procedure SetChilkatDllPath(const APath: string);

{ Returns True if the library is currently loaded. }
function ChilkatDllLoaded: Boolean;

{ Loads the library if needed and resolves ProcName to a function pointer.
  ProcName is AnsiString because DLL/SO export names are always narrow strings.
  Raises an exception if the library or the named entry point cannot be found. }
procedure Chilkat_GetProcedureAddress(var P; const ProcName: AnsiString);

{ Explicitly unloads the library. Also called automatically at finalization. }
procedure Chilkat_Dispose;

implementation

// ---------------------------------------------------------------------------
// Platform-specific uses
//
// MSWINDOWS covers both Delphi (Winapi.Windows) and FPC on Windows (Windows).
// On POSIX (Linux / macOS) FPC uses the cross-platform dynlibs unit.
// ---------------------------------------------------------------------------
{$IFDEF MSWINDOWS}
  {$IFDEF FPC}
  uses Windows, SysUtils;
  {$ELSE}
  uses Winapi.Windows, System.SysUtils;
  {$ENDIF}
{$ELSE}
uses dynlibs, SysUtils;
{$ENDIF}

// ---------------------------------------------------------------------------
// Portable library handle type
// HMODULE (Windows) and TLibHandle (dynlibs) are both NativeUInt underneath.
// ---------------------------------------------------------------------------
{$IFDEF MSWINDOWS}
type TChilkatLib = HMODULE;
{$ELSE}
type TChilkatLib = TLibHandle;
{$ENDIF}

const
  NullLib = TChilkatLib(0);

var
  hLib:         TChilkatLib;
  ckLoadedFrom: string;
  ckCustomPath: string;
  ckLock:       TRTLCriticalSection;

// ---------------------------------------------------------------------------
// Default library name — resolved entirely at compile time
// ---------------------------------------------------------------------------
function DefaultLibName: string;
begin
{$IF DEFINED(MSWINDOWS)}
  // CPUAARCH64 must be tested before WIN64 because FPC defines both on the
  // aarch64-win64 target.  Only FPC defines CPUAARCH64 on Windows.
  //
  // Delphi's "Windows on Arm" target (Delphi 13.1+) is intentionally NOT
  // directed to the arm64 DLL: Delphi WoA compiles to Arm64EC, and an
  // Arm64EC process cannot load a classic-ARM64 DLL such as
  // chilkat_c_bridge_arm64.dll.  It CAN load x64 DLLs via EC interop, so
  // Delphi WoA (which defines WIN64) correctly falls through to the x64 DLL.
  {$IF DEFINED(CPUAARCH64)}
  Result := 'chilkat_c_bridge_arm64.dll';
  {$ELSEIF DEFINED(WIN64)}
  Result := 'chilkat_c_bridge_x64.dll';
  {$ELSE}
  Result := 'chilkat_c_bridge_win32.dll';
  {$ENDIF}

{$ELSEIF DEFINED(LINUX)}
  {$IF DEFINED(CPUX86_64)}
  Result := 'libchilkat_c_bridge_x86_64.so';
  {$ELSEIF DEFINED(CPUI386)}
  Result := 'libchilkat_c_bridge_x86.so';
  {$ELSEIF DEFINED(CPUAARCH64)}
  Result := 'libchilkat_c_bridge_arm64.so';
  {$ELSEIF DEFINED(CPUARM)}
  Result := 'libchilkat_c_bridge_arm.so';
  {$ELSE}
  Result := 'libchilkat_c_bridge.so';           // fallback for unknown Linux arch
  {$ENDIF}

{$ELSEIF DEFINED(DARWIN)}
	//{$IFDEF CPUAARCH64}
  	//Result := 'libchilkat_arm64.dylib';  // Apple Silicon
	//{$ELSE}
  	//Result := 'libchilkat_x86_64.dylib'; // Intel Mac
	//{$ENDIF}
  Result := 'libchilkat_c_bridge.dylib';   // Universal (arm64 + x86_64)

{$ELSE}
  Result := 'libchilkat_c_bridge.so';           // unknown POSIX platform
{$ENDIF}
end;

// ---------------------------------------------------------------------------
// Directory containing the running executable (cross-platform)
// ---------------------------------------------------------------------------
function AppDirectory: string;
{$IFDEF MSWINDOWS}
var
  Buffer: array[0..MAX_PATH] of Char;
begin
  // GetModuleFileName is more reliable than ParamStr(0) on Windows
  // (ParamStr(0) can be empty if the process was created without a path).
  if GetModuleFileName(HInstance, Buffer, Length(Buffer)) > 0 then
    Result := ExtractFilePath(string(Buffer))
  else
    Result := ExtractFilePath(ParamStr(0));
end;
{$ELSE}
begin
  // ParamStr(0) is the standard cross-platform approach on POSIX.
  Result := ExtractFilePath(ParamStr(0));
end;
{$ENDIF}

// ---------------------------------------------------------------------------
// Thin wrappers to isolate platform API differences
// ---------------------------------------------------------------------------
function OsLoad(const Name: string): TChilkatLib;
begin
{$IFDEF MSWINDOWS}
  // LOAD_WITH_ALTERED_SEARCH_PATH: dependency DLLs are resolved relative to
  // the loaded DLL's directory rather than the calling process's directory.
  Result := LoadLibraryEx(PChar(Name), 0, LOAD_WITH_ALTERED_SEARCH_PATH);
{$ELSE}
  Result := TChilkatLib(LoadLibrary(Name));
{$ENDIF}
end;

procedure OsUnload(h: TChilkatLib);
begin
{$IFDEF MSWINDOWS}
  FreeLibrary(h);
{$ELSE}
  UnloadLibrary(h);
{$ENDIF}
end;

function OsGetProc(h: TChilkatLib; const Name: AnsiString): Pointer;
begin
{$IFDEF MSWINDOWS}
  // GetProcAddress always takes PAnsiChar on all Windows Delphi/FPC versions
  // because export table names are always narrow (ANSI) strings.
  Result := GetProcAddress(h, PAnsiChar(Name));
{$ELSE}
  Result := GetProcedureAddress(h, Name);
{$ENDIF}
end;

function OsLastError: string;
begin
  // GetLastOSError is cross-platform in SysUtils: calls GetLastError on
  // Windows and reads errno on POSIX. SysErrorMessage converts the code.
  Result := SysErrorMessage(GetLastOSError);
end;

// ---------------------------------------------------------------------------
// Internal: load the library (thread-safe, idempotent)
// ---------------------------------------------------------------------------
function Chilkat_LoadLib: TChilkatLib;
var
  TargetName: string;
  SearchPath: string;
begin
  EnterCriticalSection(ckLock);
  try
    if hLib <> NullLib then
    begin
      Result := hLib;
      Exit;
    end;

    TargetName := ckCustomPath;
    if TargetName = '' then
      TargetName := DefaultLibName;

{$IFDEF MSWINDOWS}
    // On Windows, check if this process already has the library loaded
    // (e.g. loaded by another component). Reuse the handle if so.
    hLib := TChilkatLib(GetModuleHandle(PChar(TargetName)));
    if hLib <> NullLib then
    begin
      ckLoadedFrom := TargetName + ' (already loaded)';
      Result := hLib;
      Exit;
    end;
{$ENDIF}

    // 1. Standard OS search: PATH (Windows), LD_LIBRARY_PATH (Linux),
    //    DYLD_LIBRARY_PATH (macOS), and system library directories.
    hLib := OsLoad(TargetName);
    if hLib <> NullLib then
    begin
      ckLoadedFrom := TargetName;
      Result := hLib;
      Exit;
    end;

    // 2. Same directory as the application executable.
    //    Important on Linux where '.' is not on LD_LIBRARY_PATH by default,
    //    and on macOS where the dylib may ship beside the binary.
    SearchPath := AppDirectory + ExtractFileName(TargetName);
    hLib := OsLoad(SearchPath);
    if hLib <> NullLib then
    begin
      ckLoadedFrom := SearchPath;
      Result := hLib;
      Exit;
    end;

    ckLoadedFrom := TargetName + ' [' + OsLastError + ']';
    Result := NullLib;
  finally
    LeaveCriticalSection(ckLock);
  end;
end;

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
procedure SetChilkatDllPath(const APath: string);
begin
  ckCustomPath := APath;
end;

function ChilkatDllLoaded: Boolean;
begin
  Result := hLib <> NullLib;
end;

procedure Chilkat_Dispose;
begin
  EnterCriticalSection(ckLock);
  try
    if hLib <> NullLib then
    begin
      OsUnload(hLib);
      hLib := NullLib;
      ckLoadedFrom := '';
    end;
  finally
    LeaveCriticalSection(ckLock);
  end;
end;

procedure Chilkat_GetProcedureAddress(var P; const ProcName: AnsiString);
begin
  if Chilkat_LoadLib = NullLib then
    raise Exception.Create('Chilkat library could not be loaded: ' + ckLoadedFrom);

  Pointer(P) := OsGetProc(hLib, ProcName);

  if not Assigned(Pointer(P)) then
    raise Exception.Create(
      'Entry point not found: ' + string(ProcName) + ' in ' + ckLoadedFrom);
end;

// ---------------------------------------------------------------------------
initialization
  hLib         := NullLib;
  ckLoadedFrom := '';
  ckCustomPath := '';
{$IFDEF FPC}
  InitCriticalSection(ckLock);
{$ELSE}
  InitializeCriticalSection(ckLock);
{$ENDIF}

finalization
  Chilkat_Dispose;
{$IFDEF FPC}
  DoneCriticalSection(ckLock);
{$ELSE}
  DeleteCriticalSection(ckLock);
{$ENDIF}

end.
