Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions extensions/python_api/unit_providers/low_level_bindings
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ _create_auto_provider = _import_func(
[ctypes.POINTER(ctypes.c_char_p), ctypes.c_char_p],
_unit_provider
)

_create_callback_provider = _import_func(
'${capi.get_name("create_callback_provider")}',
[ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p],
_unit_provider
)
95 changes: 95 additions & 0 deletions extensions/python_api/unit_providers/methods
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,98 @@

c_value = _create_auto_provider(input_files_arg, c_charset)
return cls(c_value)

@classmethod
def from_callback(cls, callback_fn, charset=None):
"""
Return a unit provider that calls back to Python to resolve unit names.

callback_fn should be a callable that takes (name, kind) and returns
a filename string, or None if the unit is not found.

:param callback_fn: Callable[[str, str], Optional[str]]
Takes unit_name (str) and kind ("spec" or "body")
Returns filename or None
:param charset: Character encoding for source files (default: ISO-8859-1)

.. note::
Callback references are retained for the lifetime of the application
to prevent garbage collection of the ctypes function pointers. If you
create many temporary callback providers in a long-running application,
this may increase memory usage. For typical usage patterns (a small
number of long-lived providers), this is not a concern.

Example::

def my_resolver(name, kind):
# name is typically lowercase (e.g., "ada.text_io")
# kind is "spec" or "body"
if kind == "spec":
return f"runtime/{name.replace('.', '-')}.adas"
else:
return f"runtime/{name.replace('.', '-')}.adab"

provider = UnitProvider.from_callback(my_resolver)
ctx = AnalysisContext(unit_provider=provider)
"""

# Cache libc for malloc calls (done once per provider, not per callback)
# Note: Ideally this would be module-level, but Mako template constraints
# make per-provider caching the practical choice
libc = ctypes.CDLL(None)
libc.malloc.argtypes = [ctypes.c_size_t]
libc.malloc.restype = ctypes.c_void_p

# Create a wrapper that converts from C callback to Python
@ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p,
ctypes.c_int)
def c_callback_wrapper(data_ptr, name_ptr, kind_int):
"""C callback that bridges to Python"""
try:
# Convert C string to Python
name = ctypes.string_at(name_ptr).decode('utf-8')

# Convert kind int to string
kind = "spec" if kind_int == 0 else "body"

# Call Python callback
result = callback_fn(name, kind)

# If None, return null pointer
if result is None:
return None

# Convert result to C string allocated with malloc
# Ada will call free() on this pointer after copying the string
result_bytes = result.encode('utf-8') + b'\0'
result_len = len(result_bytes)

# Allocate memory using C's malloc (libc cached in closure)
ptr = libc.malloc(result_len)
if not ptr:
return None

# Copy Python bytes to malloc'd memory
ctypes.memmove(ptr, result_bytes, result_len)

return ptr

except Exception:
# If Python callback raises, return None
_log_uncaught_error("UnitProvider.from_callback")
return None

# Keep a reference to prevent garbage collection
if not hasattr(cls, '_callback_refs'):
cls._callback_refs = []
cls._callback_refs.append(c_callback_wrapper)

# Create the provider
c_charset = _unwrap_charset(charset)
c_value = _create_callback_provider(
c_callback_wrapper,
None, # data pointer (not used in this simple version)
c_charset
)

return cls(c_value)
176 changes: 176 additions & 0 deletions extensions/src/libadalang-callback_provider.adb
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
--
-- Copyright (C) 2025, AdaCore
-- SPDX-License-Identifier: Apache-2.0
--

with Ada.Unchecked_Conversion;
with Interfaces.C; use Interfaces.C;
with Interfaces.C.Strings; use Interfaces.C.Strings;

package body Libadalang.Callback_Provider is

function Address_To_Chars_Ptr is new Ada.Unchecked_Conversion
(System.Address, chars_ptr);

function Chars_Ptr_To_Address is new Ada.Unchecked_Conversion
(chars_ptr, System.Address);

-- Import C's free() to properly free malloc'd strings
procedure C_Free (Ptr : System.Address)
with Import, Convention => C, External_Name => "free";

-----------------------
-- Get_Unit_Filename --
-----------------------

overriding function Get_Unit_Filename
(Provider : Callback_Unit_Provider;
Name : Text_Type;
Kind : Analysis_Unit_Kind) return String
is
-- Convert unit name to UTF-8 string
Name_UTF8 : constant String := To_UTF8 (Name);
Name_C : chars_ptr := New_String (Name_UTF8);

-- Convert kind to integer (0 = spec, 1 = body)
Kind_Int : constant int :=
(if Kind = Unit_Specification then 0 else 1);

-- Call Python callback
Result_Addr : System.Address;
Result_Str : Unbounded_String;
Result_C : chars_ptr;

use type System.Address;
begin
-- Call callback with the C string pointer converted to address
Result_Addr := Provider.Callback
(Provider.Data, Chars_Ptr_To_Address (Name_C), Kind_Int);
Free (Name_C);

-- If callback returned null, unit not found
if Result_Addr = System.Null_Address then
return "";
end if;

-- Convert C string address to Ada string
-- Use unchecked conversion to convert Address to chars_ptr
Result_C := Address_To_Chars_Ptr (Result_Addr);
Result_Str := To_Unbounded_String (Value (Result_C));

-- Memory Management:
-- Free the returned string. The callback must allocate with malloc().
-- The Python bindings use ctypes.malloc to ensure correct memory sharing
-- between Python and Ada/C.
C_Free (Result_Addr);

return To_String (Result_Str);
end Get_Unit_Filename;

-----------------------
-- Get_Unit_Location --
-----------------------

overriding procedure Get_Unit_Location
(Provider : Callback_Unit_Provider;
Name : Text_Type;
Kind : Analysis_Unit_Kind;
Filename : in out Unbounded_String;
PLE_Root_Index : in out Natural)
is
Fn : constant String := Provider.Get_Unit_Filename (Name, Kind);
begin
if Fn = "" then
Filename := Null_Unbounded_String;
PLE_Root_Index := 1;
else
Filename := To_Unbounded_String (Fn);
-- Limitation: PLE_Root_Index is hardcoded to 1, which assumes
-- exactly one compilation unit per file starting at the root.
-- Files with multiple compilation units are not supported.
PLE_Root_Index := 1;
end if;
end Get_Unit_Location;

--------------
-- Get_Unit --
--------------

overriding function Get_Unit
(Provider : Callback_Unit_Provider;
Context : Analysis_Context'Class;
Name : Text_Type;
Kind : Analysis_Unit_Kind;
Charset : String := "";
Reparse : Boolean := False) return Analysis_Unit'Class
is
Fn : constant String := Provider.Get_Unit_Filename (Name, Kind);
Actual_Charset : constant String :=
(if Charset'Length = 0
then To_String (Provider.Charset)
else Charset);
begin
if Fn = "" then
-- Return an empty unit if not found
declare
Empty_Unit : Analysis_Unit'Class :=
Get_From_Buffer
(Context => Context,
Filename => To_UTF8 (Name),
Buffer => "",
Charset => Actual_Charset);
begin
return Empty_Unit;
end;
else
return Context.Get_From_File (Fn, Actual_Charset, Reparse);
end if;
end Get_Unit;

---------------------------
-- Get_Unit_And_PLE_Root --
---------------------------

overriding procedure Get_Unit_And_PLE_Root
(Provider : Callback_Unit_Provider;
Context : Analysis_Context'Class;
Name : Text_Type;
Kind : Analysis_Unit_Kind;
Charset : String := "";
Reparse : Boolean := False;
Unit : in out Analysis_Unit'Class;
PLE_Root_Index : in out Natural)
is
begin
Unit := Provider.Get_Unit (Context, Name, Kind, Charset, Reparse);
PLE_Root_Index := 1;
end Get_Unit_And_PLE_Root;

-------------
-- Release --
-------------

overriding procedure Release (Provider : in out Callback_Unit_Provider) is
begin
-- Nothing to release - Python manages the callback and data
null;
end Release;

------------------------------
-- Create_Callback_Provider --
------------------------------

function Create_Callback_Provider
(Callback : Get_Unit_Filename_Callback;
Data : System.Address;
Charset : String := Default_Charset) return Callback_Unit_Provider
is
begin
return Provider : Callback_Unit_Provider do
Provider.Callback := Callback;
Provider.Data := Data;
Provider.Charset := To_Unbounded_String (Charset);
end return;
end Create_Callback_Provider;

end Libadalang.Callback_Provider;
109 changes: 109 additions & 0 deletions extensions/src/libadalang-callback_provider.ads
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
--
-- Copyright (C) 2025, AdaCore
-- SPDX-License-Identifier: Apache-2.0
--
-- This package provides a unit provider that calls back into Python
-- to resolve unit names to filenames. This allows Python code to
-- implement custom unit resolution logic without modifying libadalang.
--
-- Limitation: This provider assumes one compilation unit per file.
-- Files with multiple compilation units are not supported.

with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Interfaces.C; use Interfaces.C;
with System;

with Libadalang.Analysis; use Libadalang.Analysis;
with Libadalang.Common; use Libadalang.Common;

package Libadalang.Callback_Provider is

use Support.Text;

type Callback_Unit_Provider is
new Libadalang.Analysis.Unit_Provider_Interface with private;
-- Unit provider that calls back to Python for unit filename resolution

overriding function Get_Unit_Filename
(Provider : Callback_Unit_Provider;
Name : Text_Type;
Kind : Analysis_Unit_Kind) return String;

overriding procedure Get_Unit_Location
(Provider : Callback_Unit_Provider;
Name : Text_Type;
Kind : Analysis_Unit_Kind;
Filename : in out Unbounded_String;
PLE_Root_Index : in out Natural);

overriding function Get_Unit
(Provider : Callback_Unit_Provider;
Context : Analysis_Context'Class;
Name : Text_Type;
Kind : Analysis_Unit_Kind;
Charset : String := "";
Reparse : Boolean := False) return Analysis_Unit'Class;

overriding procedure Get_Unit_And_PLE_Root
(Provider : Callback_Unit_Provider;
Context : Analysis_Context'Class;
Name : Text_Type;
Kind : Analysis_Unit_Kind;
Charset : String := "";
Reparse : Boolean := False;
Unit : in out Analysis_Unit'Class;
PLE_Root_Index : in out Natural);

overriding procedure Release (Provider : in out Callback_Unit_Provider);

-- Callback function type for language bindings to implement
-- Parameters:
-- Data: Opaque pointer to user data (passed through from Create)
-- Name: Unit name as UTF-8 null-terminated string
-- Kind: 0 for spec, 1 for body
-- Returns: Filename as UTF-8 null-terminated string, or null for "not found"
--
-- Memory Ownership:
-- The returned string MUST be allocated with malloc(). Ada will call
-- free() on the returned pointer after copying the string value.
-- Returning NULL (System.Null_Address) indicates unit not found.
type Get_Unit_Filename_Callback is access function
(Data : System.Address;
Name : System.Address;
Kind : int) return System.Address
with Convention => C;

function Create_Callback_Provider
(Callback : Get_Unit_Filename_Callback;
Data : System.Address;
Charset : String := Default_Charset) return Callback_Unit_Provider;
-- Create a unit provider that calls back to Python.
--
-- Callback: Function pointer to Python callback
-- Data: Opaque pointer to Python object (passed to callback)
-- Charset: Character set for source files

function Create_Callback_Provider_Reference
(Callback : Get_Unit_Filename_Callback;
Data : System.Address;
Charset : String := Default_Charset) return Unit_Provider_Reference;
-- Wrapper around Create_Callback_Provider to create a unit provider reference

private

type Callback_Unit_Provider is
new Libadalang.Analysis.Unit_Provider_Interface
with record
Callback : Get_Unit_Filename_Callback;
Data : System.Address;
Charset : Unbounded_String;
end record;

function Create_Callback_Provider_Reference
(Callback : Get_Unit_Filename_Callback;
Data : System.Address;
Charset : String := Default_Charset) return Unit_Provider_Reference
is (Create_Unit_Provider_Reference
(Create_Callback_Provider (Callback, Data, Charset)));

end Libadalang.Callback_Provider;
Loading