Skip to content

Fix ArgumentOutOfRangeException in Type and Decl constructors during ObjC AST traversal#690

Merged
tannergooding merged 1 commit intodotnet:mainfrom
dalexsoto:dev/alex/fix-cursor-issue
Mar 13, 2026
Merged

Fix ArgumentOutOfRangeException in Type and Decl constructors during ObjC AST traversal#690
tannergooding merged 1 commit intodotnet:mainfrom
dalexsoto:dev/alex/fix-cursor-issue

Conversation

@dalexsoto
Copy link
Member

@dalexsoto dalexsoto commented Mar 12, 2026

Problem

When using ClangSharp to traverse Objective-C translation units that include Apple system frameworks (e.g. <Foundation/Foundation.h>), the Type constructor throws ArgumentOutOfRangeException("handle") during deep AST traversal. This makes it impossible to some Objective-C frameworks like Facebook's FBSDKCoreKit using tools built on ClangSharp.

The crash was discovered while using the sharpie bind tool (from dotnet/macios) to generate C# bindings for iOS frameworks. The specific crash path is:

TranslationUnitDecl.Decls
→ RecordDecl.Decls (C struct fields from Foundation)
→ FieldDecl.Type
→ PointerType.PointeeType (lazy evaluation)
→ Type.Create() dispatches to AttributedType based on TypeClass
→ AttributedType constructor passes CXType_Attributed as expectedKind
→ Type constructor: handle.kind (CXType_ObjCId) != expectedKind → THROW

Root Cause

The Type constructor validates CXTypeKind (from libclang) BEFORE CX_TypeClass (from libClangSharp), and throws when they don't match. However, libclang's CXTypeKind is a coarser classification than libClangSharp's CX_TypeClass. For certain Objective-C types, libclang returns a broad kind like CXType_ObjCId (27) or CXType_Unexposed (1) while libClangSharp correctly classifies the same type as CX_TypeClass_Attributed by inspecting the Clang AST directly.

The Type.Create() factory method dispatches on TypeClass (which is correct), but the resulting subclass constructor then rejects the handle because CXTypeKind doesn't match the expected value. This is a false rejection — TypeClass is the authoritative classifier and should take precedence.

A secondary issue exists in the Decl constructor: the explicit check handle.DeclKind == CX_DeclKind_Invalid causes an unconditional throw even when Decl.Create()'s default case intentionally constructs a generic Decl wrapper for unknown declaration kinds. The default case passes expectedDeclKind = handle.DeclKind = CX_DeclKind_Invalid, which then triggers the Invalid-specific guard. This makes the default case dead code that always crashes instead of gracefully degrading.

Fix

Type.cs

  • Reorder validation: check CX_TypeClass first (authoritative), then CXTypeKind (informational)
  • When TypeClass matches but CXTypeKind doesn't, accept the type instead of throwing. This handles the common case where libclang uses a broader kind (e.g. CXType_ObjCId, CXType_Unexposed, CXType_ObjCObjectPointer) for a type that libClangSharp classifies more precisely

Decl.cs

  • Remove the handle.DeclKind == CX_DeclKind_Invalid guard so that the default case in Decl.Create() can construct a generic Decl wrapper for unknown declaration kinds instead of crashing

Tests

Added 4 new tests in ObjectiveCTest.cs:

  • Type_AttributedType_WithMismatchedCXTypeKind: Parses an ObjC file with nullable id parameters using Foundation headers and deeply traverses all type information including PointeeType and AttributedType chains
  • Type_FullFoundationTraversal_DoesNotCrash: Full recursive traversal of all declarations from a Foundation-importing translation unit, including ObjCContainerDecl children and RecordDecl fields (the actual crash path)
  • Type_DeepTraversal_DoesNotCrash: Deep traversal of inline ObjC code with interfaces, protocols, categories, methods, and properties
  • Decl_InvalidDeclKind_DoesNotCrash: Traversal of diverse ObjC declaration kinds including CursorChildren access

The Foundation-based tests:

  • Try iPhoneOS SDK first (where the bug manifests), fall back to macOS SDK
  • Use xcrun clang --print-resource-dir for clang resource headers
  • Gracefully skip via Assert.Ignore if no SDK is available
  • Are already guarded by [Platform("macosx")] on the test class

…ObjC AST traversal

## Problem

When using ClangSharp to traverse Objective-C translation units that include
Apple system frameworks (e.g. `<Foundation/Foundation.h>`), the Type
constructor throws `ArgumentOutOfRangeException("handle")` during deep AST
traversal. This makes it impossible to bind large Objective-C frameworks like
Facebook's FBSDKCoreKit (219 headers) using tools built on ClangSharp.

The crash was discovered while using the `sharpie bind` tool (from
dotnet/macios) to generate C# bindings for iOS frameworks. The specific
crash path is:

    TranslationUnitDecl.Decls
    → RecordDecl.Decls (C struct fields from Foundation)
    → FieldDecl.Type
    → PointerType.PointeeType (lazy evaluation)
    → Type.Create() dispatches to AttributedType based on TypeClass
    → AttributedType constructor passes CXType_Attributed as expectedKind
    → Type constructor: handle.kind (CXType_ObjCId) != expectedKind → THROW

## Root Cause

The `Type` constructor validates `CXTypeKind` (from libclang) BEFORE
`CX_TypeClass` (from libClangSharp), and throws when they don't match.
However, libclang's `CXTypeKind` is a coarser classification than
libClangSharp's `CX_TypeClass`. For certain Objective-C types, libclang
returns a broad kind like `CXType_ObjCId` (27) or `CXType_Unexposed` (1)
while libClangSharp correctly classifies the same type as
`CX_TypeClass_Attributed` by inspecting the Clang AST directly.

The `Type.Create()` factory method dispatches on `TypeClass` (which is
correct), but the resulting subclass constructor then rejects the handle
because `CXTypeKind` doesn't match the expected value. This is a false
rejection — `TypeClass` is the authoritative classifier and should take
precedence.

A secondary issue exists in the `Decl` constructor: the explicit check
`handle.DeclKind == CX_DeclKind_Invalid` causes an unconditional throw even
when `Decl.Create()`'s default case intentionally constructs a generic Decl
wrapper for unknown declaration kinds. The default case passes
`expectedDeclKind = handle.DeclKind = CX_DeclKind_Invalid`, which then
triggers the Invalid-specific guard. This makes the default case dead code
that always crashes instead of gracefully degrading.

## Fix

### Type.cs
- Reorder validation: check `CX_TypeClass` first (authoritative), then
  `CXTypeKind` (informational)
- When `TypeClass` matches but `CXTypeKind` doesn't, accept the type
  instead of throwing. This handles the common case where libclang uses a
  broader kind (e.g. `CXType_ObjCId`, `CXType_Unexposed`,
  `CXType_ObjCObjectPointer`) for a type that libClangSharp classifies
  more precisely

### Decl.cs
- Remove the `handle.DeclKind == CX_DeclKind_Invalid` guard so that the
  default case in `Decl.Create()` can construct a generic Decl wrapper for
  unknown declaration kinds instead of crashing

## Tests

Added 4 new tests in `ObjectiveCTest.cs`:

- `Type_AttributedType_WithMismatchedCXTypeKind`: Parses an ObjC file with
  `nullable id` parameters using Foundation headers and deeply traverses
  all type information including PointeeType and AttributedType chains
- `Type_FullFoundationTraversal_DoesNotCrash`: Full recursive traversal of
  all declarations from a Foundation-importing translation unit, including
  ObjCContainerDecl children and RecordDecl fields (the actual crash path)
- `Type_DeepTraversal_DoesNotCrash`: Deep traversal of inline ObjC code
  with interfaces, protocols, categories, methods, and properties
- `Decl_InvalidDeclKind_DoesNotCrash`: Traversal of diverse ObjC
  declaration kinds including CursorChildren access

The Foundation-based tests:
- Try iPhoneOS SDK first (where the bug manifests), fall back to macOS SDK
- Use `xcrun clang --print-resource-dir` for clang resource headers
- Gracefully skip via `Assert.Ignore` if no SDK is available
- Are already guarded by `[Platform("macosx")]` on the test class

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tannergooding tannergooding merged commit e48a3ee into dotnet:main Mar 13, 2026
25 of 27 checks passed
@dalexsoto dalexsoto deleted the dev/alex/fix-cursor-issue branch March 13, 2026 22:27
dalexsoto added a commit to dotnet/macios that referenced this pull request Mar 14, 2026
When using `sharpie bind --header <file> --scope <dir>`, the --scope
argument was stored verbatim without path normalization. Since Clang
always reports declaration source locations as absolute paths, a
relative --scope value (e.g. `MyFramework.framework/Headers`) would
never match the absolute filename from `presumedLoc.FileName`, causing
IsInScope() to filter out every declaration and produce zero output
files — even though parsing succeeded.

Additionally, the StartsWith comparison in IsInScope() could produce
false positive matches when one directory name was a prefix of another
(e.g. scope `/tmp/scope` would incorrectly match files in
`/tmp/scopeextra/`).

~~Finally, when binding a very large number of Objective-C headers
(200+), ClangSharp can throw an ArgumentOutOfRangeException with
parameter name "handle" during AST traversal, due to cursor/type handle
misclassification in its managed wrapper layer. Sharpie caught this
exception but reported the raw, opaque message ("Specified argument was
out of the range of valid values"), giving users no guidance on how to
work around the issue.~~ -> Moved to
dotnet/ClangSharp#690

Changes:

1. Tools.cs: Normalize --scope paths to absolute via Path.GetFullPath()
when parsing CLI arguments, matching the behavior already used by
--framework mode (which calls Path.GetFullPath on SourceFramework in
ResolveFramework()).

2. ObjectiveCBinder.cs (IsInScope): Append a trailing directory
separator to scope directory paths before the StartsWith check, so that
`/tmp/scope/` does not falsely match `/tmp/scopeextra/`.

~~3. BindingResult.cs (ReportUnexpectedError): Detect the specific
ArgumentOutOfRangeException("handle") pattern from ClangSharp and report
an actionable error message that explains the root cause (translation
unit too complex) and suggests workarounds (bind fewer headers, use
--scope).~~

4. Tests: Added 5 new test cases:
- Scope_RelativePath: verifies relative --scope paths produce correct
output after normalization
- Scope_FiltersOutOfScopeDeclarations: verifies that only declarations
from in-scope headers are bound
- Scope_PrefixDoesNotFalseMatch: verifies that scope "/foo/bar" does not
match "/foo/barbaz/header.h"
- ~~HandleCrash_ReportsUsefulError: verifies the improved error message
for ClangSharp handle exceptions~~
- ~~HandleCrash_OtherExceptionsUnchanged: verifies that other exception
types still report their original message~~

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants