├── src
├── Sample
│ ├── Sample.csproj
│ └── Program.cs
├── NativeObjects.sln
└── NativeObjects
│ ├── NativeObjects.csproj
│ └── NativeObjectGenerator.cs
├── LICENSE
├── README.md
└── .gitignore
/src/Sample/Sample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | enable
7 | enable
8 | true
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/Sample/Program.cs:
--------------------------------------------------------------------------------
1 | namespace Sample;
2 |
3 | internal class Program
4 | {
5 | static void Main(string[] args)
6 | {
7 | var calculator = new Calculator();
8 | using var nativeCalculator = NativeObjects.ICalculator.Wrap(calculator);
9 | CallNativeCalculator(nativeCalculator.Object);
10 | }
11 |
12 | static void CallNativeCalculator(IntPtr ptr)
13 | {
14 | var calculator = NativeObjects.ICalculator.Wrap(ptr);
15 | var result = calculator.Add(2, 3);
16 |
17 | Console.WriteLine($"2 + 3 = {result}");
18 | }
19 | }
20 |
21 | [NativeObject]
22 | public interface ICalculator
23 | {
24 | int Add(int value1, int value2);
25 | }
26 |
27 | public class Calculator : ICalculator
28 | {
29 | public int Add(int value1, int value2) => value1 + value2;
30 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Kevin Gosse
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/NativeObjects.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.13.35617.110 d17.13
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeObjects", "NativeObjects\NativeObjects.csproj", "{B24509F4-DBA7-4898-B8CB-99A4D47C7519}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample", "Sample\Sample.csproj", "{41F27228-10C9-4610-B666-A2F20BA51EB0}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {B24509F4-DBA7-4898-B8CB-99A4D47C7519}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {B24509F4-DBA7-4898-B8CB-99A4D47C7519}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {B24509F4-DBA7-4898-B8CB-99A4D47C7519}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {B24509F4-DBA7-4898-B8CB-99A4D47C7519}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {41F27228-10C9-4610-B666-A2F20BA51EB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {41F27228-10C9-4610-B666-A2F20BA51EB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {41F27228-10C9-4610-B666-A2F20BA51EB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {41F27228-10C9-4610-B666-A2F20BA51EB0}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {B6B5E132-6E46-4A41-A905-FB1C26E24D02}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NativeObjects
2 |
3 | Source-generator for easier native interop.
4 |
5 | It provides two core features:
6 | - Ability to consume COM-like native objects
7 | - Ability to expose managed objects as COM-like native objects
8 |
9 | # Usage
10 |
11 | **Remember to enable unsafe on your project when referencing this source-generator**
12 |
13 | ```xml
14 |
15 | true
16 |
17 | ```
18 |
19 | Declare the interface you want to consume or expose, and decorate it with the [NativeObject] attribute:
20 |
21 | ```csharp
22 |
23 | [NativeObject]
24 | public interface ICalculator
25 | {
26 | int Add(int value1, int value2);
27 | }
28 | ```
29 |
30 | **The order of the methods is used to build the vtable. Therefore you MUST declare the methods in your interface in the same order as the native object.**
31 |
32 | From there, you can consume a native object that implements this interface:
33 |
34 | ```csharp
35 | public int DoSomething(IntPtr nativePtr)
36 | {
37 | var calc = NativeObjects.ICalculator.Wrap(nativePtr);
38 |
39 | return calc.Add(2, 3);
40 | }
41 | ```
42 |
43 | Or implement that interface then expose the managed object to native code:
44 |
45 | ```csharp
46 | public class MyCalculator : ICalculator
47 | {
48 | public int Add(int value1, int value2)
49 | {
50 | return value1 + value2;
51 | }
52 | }
53 |
54 | var calculator = new MyCalculator();
55 | using (var nativeCalculator = NativeObjects.ICalculator.Wrap(calculator))
56 | {
57 | // nativeCalculator can be implicitly cast to IntPtr
58 | // This is equivalent to calling nativeCalculator.Object
59 | SomeNativeCode((IntPtr)nativeCalculator);
60 | }
61 | ```
62 |
63 | The generated objects have the same visibility as the interface. For instance, if the interface is declared as internal, the generated objects will be internal.
64 |
65 | # Namespace
66 |
67 | By default, the interop types are emitted in the NativeObjects namespace. You can change it by adding an attribute at the assembly level:
68 |
69 | ```csharp
70 | [assembly: NativeObjectsNamespace("MyNamespace")]
71 | ```
72 |
--------------------------------------------------------------------------------
/src/NativeObjects/NativeObjects.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | latest
6 | enable
7 | true
8 | true
9 | $(TargetsForTfmSpecificContentInPackage);CustomNugetPack
10 |
11 |
12 |
13 | NativeObjects
14 | 1.4.0.0
15 | NativeObjects
16 | Kevin Gosse
17 | https://github.com/kevingosse/NativeObjects
18 | https://github.com/kevingosse/NativeObjects
19 | false
20 | MIT
21 |
22 | Source generator for native interop.
23 | Generates implementation for interfaces to expose managed objects as COM-like, or call methods on COM-like native objects.
24 |
25 | Mark invokers as readonly.
26 | Copyright 2024-$([System.DateTime]::UtcNow.ToString(yyyy))
27 | source-generator source-generation sourcegenerator COM native interop
28 | ..\..\nugets
29 | README.md
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | build/
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # ASP.NET Scaffolding
66 | ScaffoldingReadMe.txt
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | # but not Directory.Build.rsp, as it configures directory-level build defaults
86 | !Directory.Build.rsp
87 | *.sbr
88 | *.tlb
89 | *.tli
90 | *.tlh
91 | *.tmp
92 | *.tmp_proj
93 | *_wpftmp.csproj
94 | *.log
95 | *.tlog
96 | *.vspscc
97 | *.vssscc
98 | .builds
99 | *.pidb
100 | *.svclog
101 | *.scc
102 |
103 | # Chutzpah Test files
104 | _Chutzpah*
105 |
106 | # Visual C++ cache files
107 | ipch/
108 | *.aps
109 | *.ncb
110 | *.opendb
111 | *.opensdf
112 | *.sdf
113 | *.cachefile
114 | *.VC.db
115 | *.VC.VC.opendb
116 |
117 | # Visual Studio profiler
118 | *.psess
119 | *.vsp
120 | *.vspx
121 | *.sap
122 |
123 | # Visual Studio Trace Files
124 | *.e2e
125 |
126 | # TFS 2012 Local Workspace
127 | $tf/
128 |
129 | # Guidance Automation Toolkit
130 | *.gpState
131 |
132 | # ReSharper is a .NET coding add-in
133 | _ReSharper*/
134 | *.[Rr]e[Ss]harper
135 | *.DotSettings.user
136 |
137 | # TeamCity is a build add-in
138 | _TeamCity*
139 |
140 | # DotCover is a Code Coverage Tool
141 | *.dotCover
142 |
143 | # AxoCover is a Code Coverage Tool
144 | .axoCover/*
145 | !.axoCover/settings.json
146 |
147 | # Coverlet is a free, cross platform Code Coverage Tool
148 | coverage*.json
149 | coverage*.xml
150 | coverage*.info
151 |
152 | # Visual Studio code coverage results
153 | *.coverage
154 | *.coveragexml
155 |
156 | # NCrunch
157 | _NCrunch_*
158 | .*crunch*.local.xml
159 | nCrunchTemp_*
160 |
161 | # MightyMoose
162 | *.mm.*
163 | AutoTest.Net/
164 |
165 | # Web workbench (sass)
166 | .sass-cache/
167 |
168 | # Installshield output folder
169 | [Ee]xpress/
170 |
171 | # DocProject is a documentation generator add-in
172 | DocProject/buildhelp/
173 | DocProject/Help/*.HxT
174 | DocProject/Help/*.HxC
175 | DocProject/Help/*.hhc
176 | DocProject/Help/*.hhk
177 | DocProject/Help/*.hhp
178 | DocProject/Help/Html2
179 | DocProject/Help/html
180 |
181 | # Click-Once directory
182 | publish/
183 |
184 | # Publish Web Output
185 | *.[Pp]ublish.xml
186 | *.azurePubxml
187 | # Note: Comment the next line if you want to checkin your web deploy settings,
188 | # but database connection strings (with potential passwords) will be unencrypted
189 | *.pubxml
190 | *.publishproj
191 |
192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
193 | # checkin your Azure Web App publish settings, but sensitive information contained
194 | # in these scripts will be unencrypted
195 | PublishScripts/
196 |
197 | # NuGet Packages
198 | *.nupkg
199 | # NuGet Symbol Packages
200 | *.snupkg
201 | # The packages folder can be ignored because of Package Restore
202 | **/[Pp]ackages/*
203 | # except build/, which is used as an MSBuild target.
204 | !**/[Pp]ackages/build/
205 | # Uncomment if necessary however generally it will be regenerated when needed
206 | #!**/[Pp]ackages/repositories.config
207 | # NuGet v3's project.json files produces more ignorable files
208 | *.nuget.props
209 | *.nuget.targets
210 |
211 | # Microsoft Azure Build Output
212 | csx/
213 | *.build.csdef
214 |
215 | # Microsoft Azure Emulator
216 | ecf/
217 | rcf/
218 |
219 | # Windows Store app package directories and files
220 | AppPackages/
221 | BundleArtifacts/
222 | Package.StoreAssociation.xml
223 | _pkginfo.txt
224 | *.appx
225 | *.appxbundle
226 | *.appxupload
227 |
228 | # Visual Studio cache files
229 | # files ending in .cache can be ignored
230 | *.[Cc]ache
231 | # but keep track of directories ending in .cache
232 | !?*.[Cc]ache/
233 |
234 | # Others
235 | ClientBin/
236 | ~$*
237 | *~
238 | *.dbmdl
239 | *.dbproj.schemaview
240 | *.jfm
241 | *.pfx
242 | *.publishsettings
243 | orleans.codegen.cs
244 |
245 | # Including strong name files can present a security risk
246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
247 | #*.snk
248 |
249 | # Since there are multiple workflows, uncomment next line to ignore bower_components
250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
251 | #bower_components/
252 |
253 | # RIA/Silverlight projects
254 | Generated_Code/
255 |
256 | # Backup & report files from converting an old project file
257 | # to a newer Visual Studio version. Backup files are not needed,
258 | # because we have git ;-)
259 | _UpgradeReport_Files/
260 | Backup*/
261 | UpgradeLog*.XML
262 | UpgradeLog*.htm
263 | ServiceFabricBackup/
264 | *.rptproj.bak
265 |
266 | # SQL Server files
267 | *.mdf
268 | *.ldf
269 | *.ndf
270 |
271 | # Business Intelligence projects
272 | *.rdl.data
273 | *.bim.layout
274 | *.bim_*.settings
275 | *.rptproj.rsuser
276 | *- [Bb]ackup.rdl
277 | *- [Bb]ackup ([0-9]).rdl
278 | *- [Bb]ackup ([0-9][0-9]).rdl
279 |
280 | # Microsoft Fakes
281 | FakesAssemblies/
282 |
283 | # GhostDoc plugin setting file
284 | *.GhostDoc.xml
285 |
286 | # Node.js Tools for Visual Studio
287 | .ntvs_analysis.dat
288 | node_modules/
289 |
290 | # Visual Studio 6 build log
291 | *.plg
292 |
293 | # Visual Studio 6 workspace options file
294 | *.opt
295 |
296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
297 | *.vbw
298 |
299 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
300 | *.vbp
301 |
302 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
303 | *.dsw
304 | *.dsp
305 |
306 | # Visual Studio 6 technical files
307 | *.ncb
308 | *.aps
309 |
310 | # Visual Studio LightSwitch build output
311 | **/*.HTMLClient/GeneratedArtifacts
312 | **/*.DesktopClient/GeneratedArtifacts
313 | **/*.DesktopClient/ModelManifest.xml
314 | **/*.Server/GeneratedArtifacts
315 | **/*.Server/ModelManifest.xml
316 | _Pvt_Extensions
317 |
318 | # Paket dependency manager
319 | .paket/paket.exe
320 | paket-files/
321 |
322 | # FAKE - F# Make
323 | .fake/
324 |
325 | # CodeRush personal settings
326 | .cr/personal
327 |
328 | # Python Tools for Visual Studio (PTVS)
329 | __pycache__/
330 | *.pyc
331 |
332 | # Cake - Uncomment if you are using it
333 | # tools/**
334 | # !tools/packages.config
335 |
336 | # Tabs Studio
337 | *.tss
338 |
339 | # Telerik's JustMock configuration file
340 | *.jmconfig
341 |
342 | # BizTalk build output
343 | *.btp.cs
344 | *.btm.cs
345 | *.odx.cs
346 | *.xsd.cs
347 |
348 | # OpenCover UI analysis results
349 | OpenCover/
350 |
351 | # Azure Stream Analytics local run output
352 | ASALocalRun/
353 |
354 | # MSBuild Binary and Structured Log
355 | *.binlog
356 |
357 | # NVidia Nsight GPU debugger configuration file
358 | *.nvuser
359 |
360 | # MFractors (Xamarin productivity tool) working folder
361 | .mfractor/
362 |
363 | # Local History for Visual Studio
364 | .localhistory/
365 |
366 | # Visual Studio History (VSHistory) files
367 | .vshistory/
368 |
369 | # BeatPulse healthcheck temp database
370 | healthchecksdb
371 |
372 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
373 | MigrationBackup/
374 |
375 | # Ionide (cross platform F# VS Code tools) working folder
376 | .ionide/
377 |
378 | # Fody - auto-generated XML schema
379 | FodyWeavers.xsd
380 |
381 | # VS Code files for those working on multiple tools
382 | .vscode/*
383 | !.vscode/settings.json
384 | !.vscode/tasks.json
385 | !.vscode/launch.json
386 | !.vscode/extensions.json
387 | *.code-workspace
388 |
389 | # Local History for Visual Studio Code
390 | .history/
391 |
392 | # Windows Installer files from build outputs
393 | *.cab
394 | *.msi
395 | *.msix
396 | *.msm
397 | *.msp
398 |
399 | # JetBrains Rider
400 | *.sln.iml
401 |
--------------------------------------------------------------------------------
/src/NativeObjects/NativeObjectGenerator.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Microsoft.CodeAnalysis;
3 | using Microsoft.CodeAnalysis.CSharp;
4 |
5 | namespace NativeObjectGenerator;
6 |
7 | [Generator]
8 | public class NativeObjectGenerator : IIncrementalGenerator
9 | {
10 | public void Initialize(IncrementalGeneratorInitializationContext context)
11 | {
12 | // Grab assembly-level namespace from user-supplied attribute (if any)
13 | var userNamespaceProvider = context.CompilationProvider
14 | .Select((compilation, cancellationToken) =>
15 | {
16 | string namespaceValue = null;
17 |
18 | var nsAttrSymbol = compilation.GetTypeByMetadataName("NativeObjectsNamespaceAttribute");
19 |
20 | if (nsAttrSymbol != null)
21 | {
22 | foreach (var attrData in compilation.Assembly.GetAttributes())
23 | {
24 | if (SymbolEqualityComparer.Default.Equals(attrData.AttributeClass, nsAttrSymbol))
25 | {
26 | if (attrData.ConstructorArguments.Length > 0 &&
27 | attrData.ConstructorArguments[0].Value is string userValue &&
28 | !string.IsNullOrEmpty(userValue))
29 | {
30 | namespaceValue = userValue;
31 | }
32 | }
33 | }
34 | }
35 |
36 | return namespaceValue;
37 | });
38 |
39 | var nativeObjectsProvider = context.SyntaxProvider.ForAttributeWithMetadataName("NativeObjectAttribute", static (_, _) => true, Transform);
40 |
41 | var combined = nativeObjectsProvider.Combine(userNamespaceProvider);
42 |
43 | context.RegisterSourceOutput(combined, static (ctx, result) =>
44 | {
45 | var (myNativeObjectInfo, chosenNamespace) = result;
46 |
47 | if (string.IsNullOrEmpty(chosenNamespace))
48 | {
49 | chosenNamespace = "NativeObjects";
50 | }
51 |
52 | string source = $$"""
53 | namespace {{chosenNamespace}}
54 | {
55 | {{myNativeObjectInfo.Source}}
56 | }
57 | """;
58 |
59 | ctx.AddSource($"{myNativeObjectInfo.Name}.g.cs", source);
60 | });
61 |
62 | context.RegisterPostInitializationOutput(static ctx =>
63 | {
64 | ctx.AddSource("NativeObjectAttribute.g.cs", @"using System;
65 |
66 | [AttributeUsage(AttributeTargets.Interface, Inherited = false, AllowMultiple = false)]
67 | internal class NativeObjectAttribute : Attribute { }
68 |
69 | [AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)]
70 | internal class NativeObjectsNamespaceAttribute : Attribute
71 | {
72 | public NativeObjectsNamespaceAttribute(string name) { }
73 | }
74 | ");
75 | });
76 | }
77 |
78 | private static (string Name, string Source) Transform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
79 | {
80 | var sourceBuilder = new StringBuilder(@"
81 | using System;
82 | using System.Runtime.CompilerServices;
83 | using System.Runtime.InteropServices;
84 |
85 | {visibility} unsafe class {typeName} : IDisposable
86 | {
87 | private {typeName}({interfaceName} implementation)
88 | {
89 | const int delegateCount = {delegateCount};
90 |
91 | var vtable = (IntPtr*)NativeMemory.Alloc((nuint)delegateCount, (nuint)IntPtr.Size);
92 |
93 | {functionPointers}
94 |
95 | var obj = (IntPtr*)NativeMemory.Alloc((nuint)2, (nuint)IntPtr.Size);
96 | *obj = (IntPtr)vtable;
97 |
98 | var handle = GCHandle.Alloc(implementation);
99 | *(obj + 1) = GCHandle.ToIntPtr(handle);
100 |
101 | Object = (IntPtr)obj;
102 | }
103 |
104 | public IntPtr Object { get; private set; }
105 |
106 | public static {typeName} Wrap({interfaceName} implementation) => new(implementation);
107 |
108 | public static {invokerName} Wrap(IntPtr ptr) => new(ptr);
109 |
110 | public static implicit operator IntPtr({typeName} stub) => stub.Object;
111 |
112 | public void Dispose()
113 | {
114 | if (Object != IntPtr.Zero)
115 | {
116 | var target = (void**)Object;
117 | NativeMemory.Free(*target);
118 | NativeMemory.Free(target);
119 | Object = IntPtr.Zero;
120 | }
121 | }
122 |
123 | private static class Exports
124 | {
125 | {exports}
126 | }
127 | }
128 |
129 | public unsafe readonly struct {invokerName}
130 | {
131 | private readonly IntPtr _implementation;
132 |
133 | public {invokerName}(IntPtr implementation)
134 | {
135 | _implementation = implementation;
136 | }
137 |
138 | public static implicit operator IntPtr({invokerName} invoker) => invoker._implementation;
139 |
140 | private nint* VTable => (nint*)*(nint*)_implementation;
141 |
142 | {invokerFunctions}
143 | }
144 | ");
145 |
146 | var symbol = (INamedTypeSymbol)context.TargetSymbol;
147 | var interfaceName = symbol.ToString();
148 | var typeName = $"{symbol.Name}";
149 | var invokerName = $"{symbol.Name}Invoker";
150 | int delegateCount = 0;
151 | var exports = new StringBuilder();
152 | var functionPointers = new StringBuilder();
153 | var invokerFunctions = new StringBuilder();
154 | var visibility = symbol.DeclaredAccessibility.ToString().ToLower();
155 |
156 | var interfaceList = symbol.AllInterfaces.ToList();
157 | interfaceList.Reverse();
158 | interfaceList.Add(symbol);
159 |
160 | foreach (var @interface in interfaceList)
161 | {
162 | foreach (var member in @interface.GetMembers())
163 | {
164 | if (member is not IMethodSymbol method)
165 | {
166 | continue;
167 | }
168 |
169 | if (method.MethodKind == MethodKind.SharedConstructor)
170 | {
171 | continue;
172 | }
173 |
174 | var parameterList = new StringBuilder();
175 |
176 | parameterList.Append("IntPtr* self");
177 |
178 | foreach (var parameter in method.Parameters)
179 | {
180 | var isPointer = parameter.RefKind == RefKind.None ? "" : "*";
181 |
182 | parameterList.Append($", {parameter.Type}{isPointer} __arg{parameter.Ordinal}");
183 | }
184 |
185 | var isReturnPointer = method.ReturnsByRef ? "*" : "";
186 |
187 | exports.AppendLine($" [UnmanagedCallersOnly]");
188 | exports.AppendLine($" public static {method.ReturnType}{isReturnPointer} {method.Name}({parameterList})");
189 | exports.AppendLine($" {{");
190 | exports.AppendLine($" var handle = GCHandle.FromIntPtr(*(self + 1));");
191 | exports.AppendLine($" var obj = ({interfaceName})handle.Target;");
192 | exports.Append($" ");
193 |
194 | if (!method.ReturnsVoid)
195 | {
196 | if (method.ReturnsByRef)
197 | {
198 | exports.Append($"ref var result = ref ");
199 | }
200 | else
201 | {
202 | exports.Append("var result = ");
203 | }
204 | }
205 |
206 | exports.Append($"obj.{method.Name}(");
207 |
208 | for (int i = 0; i < method.Parameters.Length; i++)
209 | {
210 | if (i > 0)
211 | {
212 | exports.Append(", ");
213 | }
214 |
215 | if (method.Parameters[i].RefKind is RefKind.In)
216 | {
217 | exports.Append($"*__arg{i}");
218 | }
219 | else if (method.Parameters[i].RefKind is RefKind.Out)
220 | {
221 | exports.Append($"out var __local{i}");
222 | }
223 | else if (method.Parameters[i].RefKind is RefKind.Ref)
224 | {
225 | exports.Append($"ref *__arg{i}");
226 | }
227 | else
228 | {
229 | exports.Append($"__arg{i}");
230 | }
231 | }
232 |
233 | exports.AppendLine(");");
234 |
235 | for (int i = 0; i < method.Parameters.Length; i++)
236 | {
237 | if (method.Parameters[i].RefKind is RefKind.Out)
238 | {
239 | exports.AppendLine($" *__arg{i} = __local{i};");
240 | }
241 | }
242 |
243 | if (!method.ReturnsVoid)
244 | {
245 | if (method.ReturnsByRef)
246 | {
247 | exports.AppendLine($" return ({method.ReturnType}*)Unsafe.AsPointer(ref result);");
248 | }
249 | else
250 | {
251 | exports.AppendLine($" return result;");
252 | }
253 | }
254 |
255 | exports.AppendLine($" }}");
256 |
257 | exports.AppendLine();
258 | exports.AppendLine();
259 |
260 | functionPointers.Append($" *(vtable + {delegateCount}) = (IntPtr)(delegate* unmanaged)&Exports.{method.Name};");
287 |
288 | var returnByRef = method.ReturnsByRef ? "ref" : "";
289 |
290 | invokerFunctions.Append($"public {returnByRef} {method.ReturnType} {method.Name}(");
291 |
292 | for (int i = 0; i < method.Parameters.Length; i++)
293 | {
294 | if (i > 0)
295 | {
296 | invokerFunctions.Append(", ");
297 | }
298 |
299 | var refKind = method.Parameters[i].RefKind;
300 |
301 | switch (refKind)
302 | {
303 | case RefKind.In:
304 | invokerFunctions.Append("in ");
305 | break;
306 | case RefKind.Out:
307 | invokerFunctions.Append("out ");
308 | break;
309 | case RefKind.Ref:
310 | invokerFunctions.Append("ref ");
311 | break;
312 | }
313 |
314 | invokerFunctions.Append($"{method.Parameters[i].Type} {GetSafeName(method.Parameters[i])}");
315 | }
316 |
317 | invokerFunctions.AppendLine(")");
318 | invokerFunctions.AppendLine(" {");
319 |
320 | invokerFunctions.Append(" var __func__ = (delegate* unmanaged[Stdcall])*(VTable + {delegateCount});");
345 |
346 | invokerFunctions.Append(" ");
347 |
348 | if (method.ReturnType.SpecialType != SpecialType.System_Void)
349 | {
350 | invokerFunctions.Append($"return {returnByRef} ");
351 | }
352 |
353 | invokerFunctions.Append("__func__(_implementation");
354 |
355 | for (int i = 0; i < method.Parameters.Length; i++)
356 | {
357 | invokerFunctions.Append($", ");
358 |
359 | var refKind = method.Parameters[i].RefKind;
360 |
361 | switch (refKind)
362 | {
363 | case RefKind.In:
364 | invokerFunctions.Append("in ");
365 | break;
366 | case RefKind.Out:
367 | invokerFunctions.Append("out ");
368 | break;
369 | case RefKind.Ref:
370 | invokerFunctions.Append("ref ");
371 | break;
372 | }
373 |
374 | invokerFunctions.Append(GetSafeName(method.Parameters[i]));
375 | }
376 |
377 | invokerFunctions.AppendLine(");");
378 |
379 | invokerFunctions.AppendLine(" }");
380 |
381 | delegateCount++;
382 | }
383 | }
384 |
385 | sourceBuilder.Replace("{typeName}", typeName);
386 | sourceBuilder.Replace("{visibility}", visibility);
387 | sourceBuilder.Replace("{exports}", exports.ToString());
388 | sourceBuilder.Replace("{interfaceName}", interfaceName);
389 | sourceBuilder.Replace("{delegateCount}", delegateCount.ToString());
390 | sourceBuilder.Replace("{functionPointers}", functionPointers.ToString());
391 | sourceBuilder.Replace("{invokerFunctions}", invokerFunctions.ToString());
392 | sourceBuilder.Replace("{invokerName}", invokerName);
393 |
394 | return ($"{symbol.ContainingNamespace?.Name ?? "_"}.{symbol.Name}", sourceBuilder.ToString());
395 | }
396 |
397 | private static string GetSafeName(IParameterSymbol symbol)
398 | {
399 | var kind = SyntaxFacts.GetKeywordKind(symbol.Name);
400 | return SyntaxFacts.IsKeywordKind(kind) ? $"@{symbol.Name}" : symbol.Name;
401 | }
402 | }
--------------------------------------------------------------------------------