Skip to content

Commit 54a2443

Browse files
committed
Minor update (critical fix; compile-time only dependency)
* Fix NativeInvoke being referenced due to attribute (metadata) still being present in the final compiled assembly. This time, using Conditional trick to ensure true zero runtime dependency and zero metadata. Confirmed with unit test. * Fix Example project failing in solution Release mode, added additional configuration to aid in local development for testing NuGet package ref (run-localnuget.cmd). * Reformat files.
1 parent 79a092c commit 54a2443

29 files changed

Lines changed: 1127 additions & 291 deletions

.github/workflows/build-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252

5353
- name: Upload test results
5454
if: always()
55-
uses: actions/upload-artifact@v4
55+
uses: actions/upload-artifact@v7
5656
with:
5757
name: test-results-${{ matrix.os }}
5858
path: ./TestResults/*.trx

Example/Example.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ int MessageBoxA(HWND hWnd, ReadOnlySpan<byte> lpText, ReadOnlySpan<byte> lpCapti
6262
return MessageBoxA(hWnd, (TAnsiChar*)textPtr, (TAnsiChar*)captionPtr, uType);
6363
}
6464
}
65+
6566
int MessageBoxA(HWND hWnd, TAnsiChar* lpText, TAnsiChar* lpCaption, UINT uType);
67+
6668
//[NIMA(EnforceBlittable = false)] // TODO/FIXME: ReadOnlySpan (ref struct) is treated as non-blittable
6769
int MessageBoxW(HWND hWnd, ReadOnlySpan<TWideChar> lpText, ReadOnlySpan<TWideChar> lpCaption, UINT uType)
6870
{
@@ -73,6 +75,7 @@ int MessageBoxW(HWND hWnd, ReadOnlySpan<TWideChar> lpText, ReadOnlySpan<TWideCha
7375
return MessageBox(hWnd, (TWideChar*)textPtr, (TWideChar*)captionPtr, uType);
7476
}
7577
}
78+
7679
[NIMA("MessageBoxW")]
7780
int MessageBox(HWND hWnd, TWideChar* lpText, TWideChar* lpCaption, UINT uType);
7881
}

Example/Example.csproj

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
@@ -16,7 +16,23 @@
1616

1717
<PropertyGroup>
1818
<!-- Define custom configurations -->
19-
<Configurations>Debug;Release;Local;NuGet</Configurations>
19+
<Configurations>Debug;Release;Local;NuGet;LocalNuGet</Configurations>
20+
</PropertyGroup>
21+
22+
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
23+
<UsePackageRef>false</UsePackageRef>
24+
<!-- Standard Debug configuration -->
25+
<Optimize>false</Optimize>
26+
<DefineConstants>$(DefineConstants);DEBUG;TRACE</DefineConstants>
27+
<DebugSymbols>true</DebugSymbols>
28+
<DebugType>portable</DebugType>
29+
</PropertyGroup>
30+
31+
<PropertyGroup Condition="'$(Configuration)'=='Release'">
32+
<UsePackageRef>false</UsePackageRef>
33+
<!-- Standard Release configuration -->
34+
<Optimize>true</Optimize>
35+
<DefineConstants>$(DefineConstants);TRACE</DefineConstants>
2036
</PropertyGroup>
2137

2238
<PropertyGroup Condition="'$(Configuration)'=='Local'">
@@ -33,9 +49,62 @@
3349
<DefineConstants>$(DefineConstants);DEBUG;TRACE</DefineConstants>
3450
</PropertyGroup>
3551

52+
<PropertyGroup Condition="'$(Configuration)'=='LocalNuGet'">
53+
<UsePackageRef>true</UsePackageRef>
54+
<!-- Inherits from Debug by setting these -->
55+
<Optimize>false</Optimize>
56+
<DefineConstants>$(DefineConstants);DEBUG;TRACE</DefineConstants>
57+
<!-- Enable verbose build output for debugging -->
58+
<BuildLogParameters>$(BuildLogParameters) /v:normal</BuildLogParameters>
59+
</PropertyGroup>
60+
61+
<ItemGroup>
62+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.12.0-beta1.25218.8">
63+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
64+
<PrivateAssets>all</PrivateAssets>
65+
</PackageReference>
66+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeStyle" Version="5.0.0">
67+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
68+
<PrivateAssets>all</PrivateAssets>
69+
</PackageReference>
70+
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.201">
71+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
72+
<PrivateAssets>all</PrivateAssets>
73+
</PackageReference>
74+
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
75+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
76+
<PrivateAssets>all</PrivateAssets>
77+
</PackageReference>
78+
<PackageReference Include="Roslynator.Analyzers" Version="4.15.0">
79+
<PrivateAssets>all</PrivateAssets>
80+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
81+
</PackageReference>
82+
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.15.0">
83+
<PrivateAssets>all</PrivateAssets>
84+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
85+
</PackageReference>
86+
<PackageReference Include="Roslynator.CodeFixes" Version="4.15.0">
87+
<PrivateAssets>all</PrivateAssets>
88+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
89+
</PackageReference>
90+
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.21.0.135717">
91+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
92+
<PrivateAssets>all</PrivateAssets>
93+
</PackageReference>
94+
</ItemGroup>
95+
3696
<Choose>
3797
<When Condition="'$(UsePackageRef)'=='true'">
38-
<ItemGroup>
98+
<PropertyGroup Condition="'$(Configuration)'=='LocalNuGet'">
99+
<!-- Add local NuGet feed -->
100+
<RestoreAdditionalProjectSources>$(RestoreAdditionalProjectSources);../nupkg</RestoreAdditionalProjectSources>
101+
</PropertyGroup>
102+
<ItemGroup Condition="'$(Configuration)'!='NuGet'">
103+
<!-- Add prerelease NuGet package -->
104+
<PackageReference Include="NativeInvoke" Version="*-*"/>
105+
</ItemGroup>
106+
<ItemGroup Condition="'$(Configuration)'=='NuGet'">
107+
<!-- Add latest stable NuGet package -->
39108
<PackageReference Include="NativeInvoke" Version="*"/>
40109
</ItemGroup>
41110
</When>
@@ -45,6 +114,10 @@
45114
<!-- Include attribute source file for development/testing (simulates contentFiles behavior from NuGet package) -->
46115
<Compile Include="..\NativeInvoke\NativeImportAttribute.cs" Link="NativeInvoke\NativeImportAttribute.cs" Visible="false"/>
47116
</ItemGroup>
117+
<PropertyGroup>
118+
<!-- Define compilation constant for local development to include attributes -->
119+
<DefineConstants>$(DefineConstants);NATIVEINVOKE_SOURCE_GENERATOR</DefineConstants>
120+
</PropertyGroup>
48121
</Otherwise>
49122
</Choose>
50123

Example/run-local.cmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
cd /d "%~dp0"
33
rmdir /s /q "bin" >NUL 2>&1
44
rmdir /s /q "obj" >NUL 2>&1
5-
dotnet restore --no-cache
5+
dotnet restore --force --no-cache
66
dotnet build -c Local --no-restore --no-incremental
77
dotnet run -c Local --no-build
88
rem pause >NUL

Example/run-localnuget.cmd

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@echo off
2+
cd /d "%~dp0"
3+
rmdir /s /q "bin" >NUL 2>&1
4+
rmdir /s /q "obj" >NUL 2>&1
5+
dotnet restore --force --no-cache
6+
dotnet build -c LocalNuGet --no-incremental
7+
dotnet run -c LocalNuGet --no-build
8+
rem pause >NUL
9+
exit /b 0
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Reflection;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.Text;
8+
using NUnit.Framework;
9+
10+
namespace NativeInvoke.Tests.AttributeValidation;
11+
12+
/// <summary>
13+
/// Tests to verify that NativeInvoke attributes are properly stripped from compiled assemblies
14+
/// when consumed as a NuGet package (compile-time only dependency)
15+
/// </summary>
16+
[TestFixture]
17+
public class AttributeStrippingTests
18+
{
19+
private const string TestSourceCode = @"
20+
using System;
21+
using NativeInvoke;
22+
23+
namespace TestNamespace
24+
{
25+
public class TestClass
26+
{
27+
[NativeImport(""kernel32"")]
28+
public static partial IKernel32 Kernel32 { get; }
29+
30+
[NativeImport(""user32"")]
31+
public static partial IUser32 User32 { get; }
32+
}
33+
34+
public interface IKernel32
35+
{
36+
[NativeImportMethod(EntryPoint = ""Beep"")]
37+
int Beep(uint dwFreq, uint dwDuration);
38+
}
39+
40+
public unsafe interface IUser32
41+
{
42+
[NativeImportMethod(EntryPoint = ""MessageBoxA"")]
43+
int MessageBox(IntPtr hWnd, sbyte* lpText, sbyte* lpCaption, uint uType);
44+
}
45+
}";
46+
47+
[Test]
48+
public void CompiledAssembly_ShouldNotContainNativeImportAttributes()
49+
{
50+
// Arrange
51+
var assembly = CreateTestCompilation();
52+
53+
// Act
54+
var attributeTypes = assembly.GetTypes()
55+
.Where(t => t.Name.Contains("NativeImportAttribute"))
56+
.ToArray();
57+
58+
// Assert
59+
Assert.That(attributeTypes, Is.Empty,
60+
"NativeImportAttribute and NativeImportMethodAttribute should be stripped from compiled assembly");
61+
}
62+
63+
[Test]
64+
public void CompiledAssembly_ShouldContainGeneratedImplementationClasses()
65+
{
66+
// Arrange
67+
var assembly = CreateTestCompilation();
68+
69+
// Act
70+
var generatedTypes = assembly.GetTypes()
71+
.Where(t => t.Name.StartsWith("__Impl_"))
72+
.ToArray();
73+
74+
// Assert
75+
Assert.That(generatedTypes, Is.Not.Empty,
76+
"Generated implementation classes should be present");
77+
Assert.That(generatedTypes.Length, Is.GreaterThanOrEqualTo(2),
78+
"Should have at least 2 generated implementations (Kernel32 and User32)");
79+
}
80+
81+
[Test]
82+
public void CompiledAssembly_ShouldContainGeneratedTestClass()
83+
{
84+
// Arrange
85+
var assembly = CreateTestCompilation();
86+
87+
// Act
88+
var testClassType = assembly.GetType("TestNamespace.TestClass");
89+
90+
// Assert
91+
Assert.That(testClassType, Is.Not.Null,
92+
"Test class should be present in compiled assembly");
93+
}
94+
95+
[Test]
96+
public void GeneratedCode_ShouldNotHaveAttributeCustomAttributes()
97+
{
98+
// Arrange
99+
var assembly = CreateTestCompilation();
100+
101+
// Act
102+
var testClassType = assembly.GetType("TestNamespace.TestClass");
103+
var properties = testClassType?.GetProperties();
104+
105+
// Assert
106+
Assert.That(properties, Is.Not.Null, "Properties should exist");
107+
108+
foreach (var property in properties)
109+
{
110+
var attributes = property.GetCustomAttributes(false);
111+
var nativeImportAttributes = attributes
112+
.Where(a => a.GetType().Name.Contains("NativeImportAttribute"))
113+
.ToArray();
114+
115+
Assert.That(nativeImportAttributes, Is.Empty,
116+
$"Property '{property.Name}' should not have NativeImportAttribute in compiled assembly");
117+
}
118+
}
119+
120+
[Test]
121+
public void GeneratedInterfaces_ShouldNotHaveAttributeCustomAttributes()
122+
{
123+
// Arrange
124+
var assembly = CreateTestCompilation();
125+
126+
// Act
127+
var kernel32Type = assembly.GetType("TestNamespace.IKernel32");
128+
var user32Type = assembly.GetType("TestNamespace.IUser32");
129+
130+
// Assert
131+
Assert.That(kernel32Type, Is.Not.Null, "IKernel32 interface should be present");
132+
Assert.That(user32Type, Is.Not.Null, "IUser32 interface should be present");
133+
134+
var kernel32Methods = kernel32Type.GetMethods();
135+
var user32Methods = user32Type.GetMethods();
136+
137+
foreach (var method in kernel32Methods.Concat(user32Methods))
138+
{
139+
var attributes = method.GetCustomAttributes(false);
140+
var nativeImportMethodAttributes = attributes
141+
.Where(a => a.GetType().Name.Contains("NativeImportMethodAttribute"))
142+
.ToArray();
143+
144+
Assert.That(nativeImportMethodAttributes, Is.Empty,
145+
$"Method '{method.Name}' should not have NativeImportMethodAttribute in compiled assembly");
146+
}
147+
}
148+
149+
[Test]
150+
public void AssemblyMetadata_ShouldIndicateDevelopmentDependency()
151+
{
152+
// Arrange
153+
var assembly = CreateTestCompilation();
154+
155+
// Act
156+
var referencedAssemblies = assembly.GetReferencedAssemblies()
157+
.Where(r => r.Name.Contains("NativeInvoke"))
158+
.ToArray();
159+
160+
// Assert
161+
Assert.That(referencedAssemblies, Is.Empty,
162+
"Compiled assembly should not reference NativeInvoke assemblies (development dependency)");
163+
}
164+
165+
private static Assembly CreateTestCompilation()
166+
{
167+
// Create syntax tree from test source
168+
var syntaxTree = CSharpSyntaxTree.ParseText(TestSourceCode);
169+
170+
// Define compilation options
171+
var compilationOptions = new CSharpCompilationOptions(
172+
OutputKind.DynamicallyLinkedLibrary,
173+
optimizationLevel: OptimizationLevel.Release,
174+
allowUnsafe: true);
175+
176+
// Create compilation with necessary references
177+
var compilation = CSharpCompilation.Create(
178+
"TestAssembly",
179+
new[] { syntaxTree },
180+
GetMetadataReferences(),
181+
compilationOptions);
182+
183+
// Add the NativeInvoke source generator
184+
var generator = new NativeInvoke.Generator.NativeImportGenerator();
185+
var driver = CSharpGeneratorDriver.Create(generator);
186+
var x = driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, out _);
187+
return EmitAssembly((CSharpCompilation)updatedCompilation);
188+
}
189+
190+
private static PortableExecutableReference[] GetMetadataReferences()
191+
{
192+
var assemblies = new[]
193+
{
194+
typeof(object).Assembly, // System.Runtime
195+
typeof(Attribute).Assembly, // System.Runtime
196+
typeof(System.Runtime.CompilerServices.NullableAttribute).Assembly, // System.Runtime
197+
typeof(System.IntPtr).Assembly, // System.Runtime.InteropServices
198+
typeof(System.Runtime.InteropServices.NativeLibrary).Assembly,
199+
};
200+
201+
return assemblies
202+
.Select(a => MetadataReference.CreateFromFile(a.Location))
203+
.ToArray();
204+
}
205+
206+
private static Assembly EmitAssembly(CSharpCompilation compilation)
207+
{
208+
using var ms = new MemoryStream();
209+
var result = compilation.Emit(ms);
210+
211+
if (!result.Success)
212+
{
213+
var errors = string.Join(Environment.NewLine,
214+
result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error));
215+
throw new InvalidOperationException($"Compilation failed: {errors}");
216+
}
217+
218+
return Assembly.Load(ms.ToArray());
219+
}
220+
}
221+
222+
/// <summary>
223+
/// Extension methods for compilation testing
224+
/// </summary>
225+
public static class CompilationExtensions
226+
{
227+
public static Assembly EmitAssembly(this CSharpCompilation compilation)
228+
{
229+
using var ms = new MemoryStream();
230+
var result = compilation.Emit(ms);
231+
232+
if (!result.Success)
233+
{
234+
var errors = string.Join(Environment.NewLine,
235+
result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error));
236+
throw new InvalidOperationException($"Compilation failed: {errors}");
237+
}
238+
239+
return Assembly.Load(ms.ToArray());
240+
}
241+
}

0 commit comments

Comments
 (0)