ASTools.NSSharp
0.1.6
dotnet tool install --global ASTools.NSSharp --version 0.1.6
dotnet new tool-manifest
dotnet tool install --local ASTools.NSSharp --version 0.1.6
#tool dotnet:?package=ASTools.NSSharp&version=0.1.6
nuke :add-package ASTools.NSSharp --version 0.1.6
NSSharp
A .NET 10 command-line tool that parses Objective-C header files and produces either a JSON representation of the API surface or C# binding definitions compatible with Xamarin.iOS / .NET for iOS.
Built as a self-contained C# parser — no libclang or native dependencies required.
Features
- Custom ObjC lexer & parser — tokenizes and parses Objective-C headers directly in C#
- UPPER_SNAKE_CASE macro heuristic — auto-detects and skips vendor macros (e.g.
PSPDF_EXPORT,FB_INIT) without hardcoded lists - NS_ASSUME_NONNULL scope tracking — accurately infers
[NullAllowed]based on nonnull scope - JSON output — structured JSON representation of all API constructs
- C# binding generation — produces Xamarin/MAUI-style
[Export]/[BaseType]binding definitions - Sharpie parity benchmark — 88.9% exact export match vs Objective Sharpie on PSPDFKitUI.xcframework (550 common exports in current DemoFramework dataset)
- Category merging — ObjC categories are merged into parent class interfaces (including
SWIFT_EXTENSIONcategories) - XCFramework support — discovers and parses all headers inside
.xcframeworkbundles - Comprehensive construct support:
@interface(classes, categories, extensions)@protocol(with@required/@optional)@property(attributes, nullability, custom getter/setter, weak→NullAllowed)- Instance and class methods
NS_ENUM/NS_OPTIONS/NS_CLOSED_ENUM/NS_ERROR_ENUM/ plain C enums (with backing types)- Structs and typedefs
- C function declarations and extern constants (
[Field]) - Block types
- Forward declarations (
@class,@protocol) - Lightweight generics (
NSArray<NSString *>, generic superclasses) - Nullability annotations (
nullable,_Nullable,__nullable, etc.)
- Configurable macro handling —
--extern-macrosfor vendor export macros,--no-macro-heuristicto disable auto-detection,--emit-c-bindingsto include C function DllImport declarations - Packaged as a dotnet tool — installable via
dotnet tool install
Requirements
Building
dotnet build
Running
Generate C# bindings (default)
# Single header → C# bindings to stdout
dotnet run -- MyHeader.h
# From an xcframework
dotnet run -- --xcframework path/to/MyLib.xcframework
# Write to file
dotnet run -- MyHeader.h -o ApiDefinition.cs
# Multiple headers
dotnet run -- Header1.h Header2.h Header3.h -o Bindings.cs
# One .cs file per header (requires output directory)
dotnet run -- --xcframework path/to/MyLib.xcframework --split-by-header -o GeneratedBindings/
# Add explicit namespace for generated C#
dotnet run -- MyHeader.h --namespace MyCompany.Bindings -o ApiDefinition.cs
Namespace behavior for C# output:
- Single header input: no namespace unless
--namespaceis provided. --xcframeworkinput: defaults to the xcframework name (e.g.PSPDFKitUI) unless--namespaceis provided.
Parse headers to JSON
# JSON output
dotnet run -- MyHeader.h -f json
# Compact JSON to file
dotnet run -- MyHeader.h -f json --compact -o output.json
# xcframework to JSON
dotnet run -- --xcframework MyLib.xcframework -f json
CLI Reference
nssharp [<files>...] [options]
Arguments:
<files> One or more Objective-C header files to parse.
Options:
--xcframework Path to an .xcframework bundle to discover and parse all headers.
--slice Select a specific xcframework slice (e.g. ios-arm64).
--list-slices List available slices in the xcframework and exit.
-o, --output Write output to a file instead of stdout (or a directory with --split-by-header).
-f, --format Output format: csharp (default) or json.
--split-by-header For csharp format, write one .cs file per input header (requires --output directory).
--namespace Namespace for generated C# output. Defaults to xcframework name when --xcframework is used.
--compact Output compact JSON (only applies to json format).
--extern-macros Comma-separated macros to treat as extern (e.g. PSPDF_EXPORT,FB_EXTERN).
--emit-c-bindings Include C function declarations ([DllImport]) in output. Extern constants ([Field]) are always included.
--no-macro-heuristic Disable UPPER_SNAKE_CASE auto-detection of vendor macros.
-h, --help Show help and usage information.
--version Show version information.
JSON Output Schema
Each header file produces a JSON object:
{
"file": "MyHeader.h",
"interfaces": [
{
"name": "MyClass",
"superclass": "NSObject",
"protocols": ["NSCoding"],
"category": null,
"properties": [
{
"name": "title",
"type": "NSString *",
"attributes": ["nonatomic", "copy"],
"isNullable": false
}
],
"instanceMethods": [
{
"selector": "initWithTitle:",
"returnType": "instancetype",
"parameters": [
{ "name": "title", "type": "NSString *", "isNullable": false }
],
"isOptional": false
}
],
"classMethods": []
}
],
"protocols": [
{
"name": "MyDelegate",
"inheritedProtocols": ["NSObject"],
"properties": [],
"requiredInstanceMethods": [],
"requiredClassMethods": [],
"optionalInstanceMethods": [],
"optionalClassMethods": []
}
],
"enums": [
{
"name": "MyStatus",
"backingType": "NSInteger",
"isOptions": false,
"values": [
{ "name": "MyStatusOK", "value": "0" },
{ "name": "MyStatusFail" }
]
}
],
"structs": [
{
"name": "MyPoint",
"fields": [
{ "name": "x", "type": "CGFloat" },
{ "name": "y", "type": "CGFloat" }
]
}
],
"typedefs": [
{ "name": "CompletionHandler", "underlyingType": "void (^)(BOOL success)" }
],
"functions": [
{
"name": "NSStringFromMyStatus",
"returnType": "NSString *",
"parameters": [
{ "name": "status", "type": "MyStatus", "isNullable": false }
]
}
],
"forwardDeclarations": {
"classes": ["NSData", "NSError"],
"protocols": []
}
}
When multiple headers are parsed, the output is a JSON array of header objects.
C# Binding Output
The --format csharp mode generates Xamarin.iOS / .NET for iOS binding-style API definitions. Example input:
@interface MyClass : NSObject <NSCoding>
@property (nonatomic, copy) NSString *title;
@property (nonatomic, readonly) NSInteger count;
+(instancetype)sharedInstance;
-(void)performAction:(NSString *)action withCompletion:(void (^)(BOOL))handler;
@end
Generated output:
using Foundation;
// @interface MyClass : NSObject
[BaseType (typeof (NSObject))]
interface MyClass : INSCoding
{
// @property (nonatomic, copy) NSString * title;
[Export ("title", ArgumentSemantic.Copy)]
string Title { get; set; }
// @property (nonatomic, readonly) NSInteger count;
[Export ("count")]
nint Count { get; }
// +(instancetype)sharedInstance;
[Static]
[Export ("sharedInstance")]
instancetype SharedInstance ();
// -(void)performAction:withCompletion:;
[Export ("performAction:withCompletion:")]
void PerformActionWithCompletion (string action, Action handler);
}
Binding generation rules
| ObjC Construct | C# Output |
|---|---|
@interface Foo : Bar |
[BaseType(typeof(Bar))] interface Foo |
@interface Foo (Cat) |
Merged into parent Foo interface (or [Category] if parent not parsed) |
@protocol P |
interface IP {} stub + [Protocol] interface P |
@protocol PDelegate |
interface IPDelegate {} stub + [Protocol, Model] [BaseType(typeof(NSObject))] interface PDelegate |
Protocol conformance <P> |
: IP interface inheritance |
@required methods |
[Abstract] [Export("sel")] |
@optional methods |
[Export("sel")] (no [Abstract]) |
@property |
[Export("name")] Type Name { get; set; } |
@property (readonly) |
{ get; } only |
@property (copy) |
ArgumentSemantic.Copy |
@property (nullable) |
[NullAllowed] |
@property (weak) |
[NullAllowed] (implicit) |
@property (class) |
[Static] |
| Object pointer property | ArgumentSemantic.Strong (readwrite default) |
Instance method - |
[Export("selector:")] — smart method naming (first part only, strips trailing prepositions) |
Class method + |
[Static] [Export("selector:")] |
Delegate method obj:didX: |
Strips sender prefix, uses second part (DidX) |
-(instancetype)init* |
NativeHandle Constructor(...) |
NS_DESIGNATED_INITIALIZER |
[DesignatedInitializer] on constructors |
| Init unavailable macro | [DisableDefaultCtor] (e.g., PSPDF_EMPTY_INIT_UNAVAILABLE) |
Static factory classWithParam: |
From<Param> (when class name matches) or Create<Name> |
Block-type property void (^name)(...) |
Property with Action type |
Method name with Block |
Renamed to Action (e.g., performBlock: → PerformAction) |
isEqualTo<Class>: |
IsEqualTo (class name suffix stripped) |
NS_ENUM(NSInteger, X) |
[Native] enum X : long |
NS_OPTIONS(NSUInteger, X) |
[Flags] enum X : ulong |
NS_CLOSED_ENUM |
Same as NS_ENUM |
NS_ERROR_ENUM |
Same as NS_ENUM |
C enum Foo : type |
enum Foo : mappedType |
struct |
[StructLayout(LayoutKind.Sequential)] struct |
extern function |
[DllImport("__Internal")] static extern (requires --emit-c-bindings) |
extern constant |
[Field("name", "__Internal")] in Constants interface |
extern NSNotificationName |
[Notification] [Field("name")] |
| Completion handler method | [Async] attribute (class methods only, auto-detected from completion: suffix) |
Protocol @required @property |
[Abstract] + C# property (with [Bind("isX")] for custom getters) |
Protocol @optional @property |
Decomposed into getter/setter method pairs |
Protocol @optional @property (getter=isX) |
Getter uses custom selector, setter uses setX: |
Variadic ... |
IntPtr varArgs parameter |
NS_ASSUME_NONNULL_BEGIN/END |
Scope-aware [NullAllowed] inference |
Type mapping
| Objective-C | C# |
|---|---|
void |
void |
BOOL |
bool |
char / signed char |
sbyte |
unsigned char |
byte |
short |
short |
unsigned short |
ushort |
int |
int |
unsigned int |
uint |
long |
nint |
unsigned long |
nuint |
long long |
long |
unsigned long long |
ulong |
float |
float |
double |
double |
NSInteger |
nint |
NSUInteger |
nuint |
CGFloat |
nfloat |
id |
NSObject |
SEL |
Selector |
Class |
Class |
instancetype |
Class name (static) or NativeHandle Constructor (init) |
NSString * |
string |
NSArray * |
NSObject [] |
NSArray<Type *> * |
Type [] (typed arrays) |
NSDictionary<K, V> |
NSDictionary<MappedK, MappedV> (preserves Foundation types) |
NSSet<T> |
NSSet<MappedT> (preserves Foundation types) |
id<Protocol> |
IProtocol |
UIView<Protocol> |
IProtocol |
IBAction |
void |
IBInspectable BOOL |
bool (IB annotations stripped) |
CGColorRef |
CGColor |
dispatch_queue_t |
DispatchQueue |
*Block typedef |
*Handler (.NET convention) |
out NSString *…* |
out string (ObjC out qualifier stripped, no double out) |
NSError ** |
out NSError + always [NullAllowed] |
See Binding/ObjCTypeMapper.cs for the full mapping table (70+ types).
XCFramework Support
The --xcframework option discovers headers inside .xcframework bundles:
dotnet run -- --xcframework MyLib.xcframework
# List available slices
dotnet run -- --xcframework MyLib.xcframework --list-slices
# Use a specific slice
dotnet run -- --xcframework MyLib.xcframework --slice ios-arm64
The tool:
- Scans the xcframework for platform slices
- Prefers the current platform (macOS on macOS, iOS on iOS)
- Locates the
Headers/directory inside framework bundles - Parses all
.hfiles found
Installing as a dotnet tool
# Pack
dotnet pack src/NSSharp/NSSharp.csproj -c Release
# Install globally from local package
dotnet tool install --global --add-source src/NSSharp/bin/Release ASTools.NSSharp
# Or install from NuGet.org
dotnet tool install --global ASTools.NSSharp
# NuGet package
# https://www.nuget.org/packages/ASTools.NSSharp/
# Use
nssharp MyHeader.h
nssharp MyHeader.h -o ApiDefinition.cs
nssharp --xcframework MyLib.xcframework -o Bindings.cs
nssharp MyHeader.h -f json -o output.json
# Uninstall
dotnet tool uninstall --global ASTools.NSSharp
Versioning & CI
Versioning is handled by Nerdbank.GitVersioning with CalVer-style release tags:
- Dev builds: Automatically versioned as
0.1.{git-height}-g{commit}(prerelease) - Releases: Create a GitHub release with a CalVer tag (e.g.
2026.02.12.1) — the tag becomes the NuGet package version
GitHub Actions CI (.github/workflows/ci.yml):
| Trigger | Build & Pack | Test | Publish to NuGet |
|---|---|---|---|
Push to main |
✅ | ✅ | ❌ |
| Pull request | ✅ | ✅ | ❌ |
| GitHub release | ✅ | ❌ | ✅ |
To publish a release:
- Create a GitHub release with a CalVer tag (e.g.
2026.02.12.1) - CI builds, packs with that version, and pushes to NuGet.org
- Requires
NUGET_ORG_API_KEYrepository secret
AI Agent Skills
NSSharp ships with two agent skills in .agents/skills/ that give AI coding assistants (e.g. Claude) specialized knowledge of this tool:
| Skill | Triggers on | What it provides |
|---|---|---|
| nssharp-objc-parser | Parsing ObjC headers, JSON output, xcframework analysis | CLI reference, supported constructs, programmatic API, JSON schema |
| nssharp-binding-generator | Generating C# bindings, type mapping, interop code | Binding rules, type mapping table (70+ types), selector conversion |
Each skill follows the progressive disclosure pattern — the SKILL.md body loads only when triggered, and detailed reference docs (JSON schema, full type mapping) load only when needed.
Project Structure
NSSharp/
├── NSSharp.slnx
├── README.md
├── AGENTS.md
├── global.json # .NET 10 SDK pin
├── version.json # Nerdbank.GitVersioning (CalVer)
├── .github/workflows/ci.yml # CI: build, test, publish
├── .agents/skills/
│ ├── nssharp-objc-parser/ # Skill for parsing ObjC headers
│ │ ├── SKILL.md
│ │ └── references/json-schema.md
│ └── nssharp-binding-generator/ # Skill for generating C# bindings
│ ├── SKILL.md
│ └── references/type-mapping.md
└── src/
├── NSSharp/
│ ├── Ast/
│ │ └── ObjCNodes.cs # AST model types
│ ├── Lexer/
│ │ ├── Token.cs # Token types and TokenKind enum
│ │ ├── ObjCLexer.cs # Tokenizer (UPPER_SNAKE_CASE macro heuristic)
│ │ └── ObjCLexerOptions.cs # Lexer config (heuristic, extern macros)
│ ├── Parser/
│ │ └── ObjCParser.cs # Recursive-descent parser
│ ├── Json/
│ │ └── ObjCJsonSerializer.cs # System.Text.Json serialization
│ ├── Binding/
│ │ ├── ObjCTypeMapper.cs # ObjC→C# type mapping
│ │ └── CSharpBindingGenerator.cs # C# binding code generation
│ ├── XCFrameworkResolver.cs # XCFramework header discovery
│ ├── Program.cs # CLI entry point (System.CommandLine)
│ └── NSSharp.csproj
└── NSSharp.Tests/
├── LexerTests.cs
├── ParserTests.cs
├── JsonSerializerTests.cs
├── SharpieScenarioTests.cs # Tests from dotnet/macios sharpie PR
├── BindingGeneratorTests.cs
├── VendorMacroScenarioTests.cs # Macro heuristic & real-world scenarios
└── NSSharp.Tests.csproj
Testing
dotnet test
189 tests covering:
- Lexer: tokenization, comment skipping, macro heuristic, UPPER_SNAKE_CASE detection, number literals
- Parser: all ObjC constructs (interfaces, protocols, properties, methods, enums, structs, typedefs, functions, blocks, categories, generics, generic superclasses, forward declarations, NS_ASSUME_NONNULL scoping)
- JSON serializer: schema correctness, camelCase, compact mode
- Binding generator: type mapping,
[Export],[BaseType],[Protocol],[Protocol, Model], I-prefix stubs,[Category], constructors,[DisableDefaultCtor], properties, enums (including NS_ERROR_ENUM), structs, P/Invoke,[Field],[Notification],[Async],[Bind],[Abstract], protocol property decomposition, category property decomposition, NullAllowed inference, out NSError NullAllowed, ArgumentSemantic (explicit only), acronym normalization, generic collection types, ObjC direction qualifier stripping, enum prefix stripping with short prefix fallback - Sharpie scenarios: 33 tests ported from dotnet/macios PR #24622 test headers
- Vendor macro scenarios: macro heuristic detection, real-world vendor macro patterns, category merging, generic superclasses, preprocessor directives in protocol lists, SWIFT_EXTENSION categories
Limitations
- No preprocessor: does not run a full C preprocessor. Vendor macros are auto-detected via UPPER_SNAKE_CASE heuristic and skipped. Use
--extern-macrosfor vendor export macros,--no-macro-heuristicto disable auto-detection. - No expression evaluation: enum values with complex expressions are preserved as strings, not evaluated.
- No C++ support:
__cplusplusguarded code, C++ classes, templates, and namespaces are out of scope. - No semantic analysis: the parser works syntactically. It does not resolve types across headers or validate type correctness.
- Not implemented:
[Wrap](convenience wrappers), Keys/Options typed dictionaries, typed[Notification(typeof(EventArgs))]with userInfo key extraction. - Protocol property decomposition:
@requiredprotocol properties get[Abstract]and stay as C# properties (with[Bind("isX")]for custom getters).@optionalprotocol properties are decomposed into explicit getter/setter method pairs, with custom getter selectors used when available. - Binding output is a starting point: generated C# bindings may need manual review and adjustment (similar to Objective Sharpie's
[Verify]hints).
License
See repository root for license information.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.