{"id":21281734,"url":"https://github.com/prophetlamb/tupleoverloadgenerator","last_synced_at":"2025-07-11T10:33:57.996Z","repository":{"id":61488940,"uuid":"551828121","full_name":"ProphetLamb/TupleOverloadGenerator","owner":"ProphetLamb","description":"Overload `params` array parameter with tuples avoiding heap allocations.","archived":false,"fork":false,"pushed_at":"2022-10-17T21:16:55.000Z","size":136,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-11-08T20:45:43.798Z","etag":null,"topics":["csharp","csharp-sourcegenerator","library","overloading","source-generator","tuple"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ProphetLamb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-10-15T07:09:59.000Z","updated_at":"2024-07-31T07:20:45.000Z","dependencies_parsed_at":"2022-10-19T22:15:32.396Z","dependency_job_id":null,"html_url":"https://github.com/ProphetLamb/TupleOverloadGenerator","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ProphetLamb%2FTupleOverloadGenerator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ProphetLamb%2FTupleOverloadGenerator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ProphetLamb%2FTupleOverloadGenerator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ProphetLamb%2FTupleOverloadGenerator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ProphetLamb","download_url":"https://codeload.github.com/ProphetLamb/TupleOverloadGenerator/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225715932,"owners_count":17512909,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","csharp-sourcegenerator","library","overloading","source-generator","tuple"],"created_at":"2024-11-21T10:50:12.093Z","updated_at":"2024-11-21T10:50:12.861Z","avatar_url":"https://github.com/ProphetLamb.png","language":"C#","readme":"\n\u003cp align=\"center\"\u003e\n\t\u003ca href=\"https://www.nuget.org/packages/TupleOverloadGenerator\"\u003e\u003cimg src=\"https://img.shields.io/nuget/dt/TupleOverloadGenerator?label=Generator\u0026style=for-the-badge\" /\u003e\u003c/a\u003e\n\t\u003ca href=\"https://www.nuget.org/packages/TupleOverloadGenerator.Types\"\u003e\u003cimg src=\"https://img.shields.io/nuget/dt/TupleOverloadGenerator.Types?label=Generator.Types\u0026style=for-the-badge\" /\u003e\u003c/a\u003e\n\t\u003cbr/\u003e\n\t\u003cimg src=\"img/banner.png\" alt=\"Logo\" width=\"305\" height=\"125\"\u003e\n\u003c/p\u003e\n\u003ch1 align=\"center\"\u003eTupleOverloadGenerator\u003c/h1\u003e\n\u003cp align=\"center\"\u003eOverload \u003ccode\u003eparams\u003c/code\u003e array parameter with tuples avoiding heap allocations.\u003c/p\u003e\n\n## Table of Contents\n\n- [Table of Contents](#table-of-contents)\n- [Supported](#supported)\n- [Motivation](#motivation)\n\t- [Stackalloc](#stackalloc)\n\t- [Pool](#pool)\n\t- [Tuple](#tuple)\n- [Example](#example)\n\t- [Generated](#generated)\n- [Behind the scenes](#behind-the-scenes)\n\t- [Tuple as Span](#tuple-as-span)\n\t- [GetPinnableReference](#getpinnablereference)\n\t- [AsSpan \u0026 AsRoSpan](#asspan--asrospan)\n\n## Supported\n\n[Generator](https://www.nuget.org/packages/TupleOverloadGenerator) and [Types](https://www.nuget.org/packages/TupleOverloadGenerator.Types) NuGet packages are required:\n```xml\n\u003cPackageReference Include=\"TupleOverloadGenerator.Types\" Version=\"1.0.2\" /\u003e\n\u003cPackageReference Include=\"TupleOverloadGenerator\" Version=\"1.0.2\" ReferenceOutputAssembly=\"false\" OutputItemType=\"Analyzer\" /\u003e\n```\n\n* .NET Framework 4.8 or greater\n* .NET 6.0 or greater\n* .NETStandard 2.1 compatible. Depends on [System.Memory](https://www.nuget.org/packages/System.Memory).\n\n## Motivation\n\nWhen producing a library we often wish to allow a variable number of arguments to be passed to a given function, such as string `Concat`enation.\nHistorically the `params` keyword followed by an array type (e.g. `params string[]`) has been to conveniently introduce a parameter with a variable number of arguments.\nHowever an array introduces a few problems, the most prevalent of which is that the array is allocated on the heap.\n\n### Stackalloc\n\nModern libraries should therefore allow the user to pass a `Span` instead of an array, this approach is the most performant, yet calling the function is inconvenient and still requires a heap allocation for managed, and non-blittable types, where `stackalloc` is not usable.\n\n| **DON'T** |\n| --------- |\n```csharp\nSpan\u003cstring\u003e parts = stackalloc string[12];\nparts[0] = \"Hello\";\nparts[1] = [...]\nAffixConcat concat = new(\"[\", \"]\");\nreturn concat.Concat(parts);\n```\n\n### Pool\n\nAlternatively an `ArrayPool` can be used, in the best case reducing the number of allocations from `n` to `1` for any given size, where `n` is the number of calls to the function. We still have allocations, and the syntax becomes even more unwieldy.\n\n| **DON'T** |\n| --------- |\n```csharp\nvar poolArray = ArrayPool\u003cstring\u003e.Shared.Rent(12);\nSpan\u003cstring\u003e parts = poolArray.AsSpan(0, 12);\nparts[0] = \"Hello\";\nparts[1] = [...]\nAffixConcat concat = new(\"[\", \"]\");\nvar result = concat.Concat(parts);\nArrayPool\u003cstring\u003e.Shared.Return(poolArray);\n```\n\n### Tuple\n\nThe solution is overloads with inline arrays. These can be represented by tuples with a single generic type parameter used for different arities. `TupleOverloadGenerator` generates these overloads for parameter array parameters decorated with the `[TupleOverload]` attribute.\nThe avoids any heap allocation at all, because we can assign any type `T` to a tuple item. `struct ValueTuple\u003cT, ...\u003e` is the underlaying type of `(T, ...)`. By default the struct lives on the stack.\n\n| **DO** |\n| ------ |\n```csharp\nAffixConcat concat = new(\"[\", \", \", \"]\");\nreturn concat.Concat((\"Hello\", [...]));\n```\n\n## Example\n\n\n```csharp\nusing System;\nusing System.Text;\n\nnamespace BasicUsageExamples;\n\npublic partial readonly record struct AffixConcat(string Prefix, string Infix, string Suffix) {\n    public string Concat(ReadOnlySpan\u003cstring\u003e parts) {\n        StringBuilder sb = new();\n        sb.Append(Prefix);\n        var en = parts.GetEnumerator();\n        if (en.MoveNext()) {\n            sb.Append(en.Current);\n            while (en.MoveNext()) {\n                sb.Append(Infix);\n                sb.Append(en.Current);\n            }\n        }\n        sb.Append(Suffix);\n        return sb.ToString();\n    }\n\n    public string Concat([TupleOverload] params string[] parts) {\n        ReadOnlySpan\u003cstring\u003e partsSpan = parts.AsSpan();\n        return Concat(partsSpan);\n    }\n}\n```\n\nThe above example displays the conditions required to use the sourcegenerator.\n\n1. A namespace directly containing a type definition. Omitted namespace doesnt allow partial definitions, and nested types are not supported.\n2. The partial type definition, e.g. `sealed partial class`, `partial record`, `partial ref struct`, ...\n3. A method with a [parameter array](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/params), e.g. `params string[]`.\n4. The parameter array decorated with the `[TupleOverload]` attribute.\n5. The parameter **exclusively** called with [allowed methods](#behind-the-scenes). **No other member can be used!**\n\nPlease note that the example above is for demonstration purposes only! I advise using a `ref struct` and a `ValueStringBuilder` for real world applications.\n\nThe following file is generated for the example above.\n\n### Generated\n\n```csharp\nusing System;\nusing System.Text;\n\nnamespace BasicUsageExamples;\n\npublic partial readonly record struct AffixConcat {\n    public string Concat([TupleOverload] ValueTuple\u003cstring\u003e parts) {\n        ReadOnlySpan\u003cstring\u003e partsSpan = parts.AsSpan();\n        return Concat(partsSpan);\n    }\n    public string Concat([TupleOverload] (string, string) parts) {\n        ReadOnlySpan\u003cstring\u003e partsSpan = parts.AsSpan();\n        return Concat(partsSpan);\n    }\n    [...]\n    public string Concat([TupleOverload] (string, [...through 21]) parts) {\n        ReadOnlySpan\u003cstring\u003e partsSpan = parts.AsSpan();\n        return Concat(partsSpan);\n    }\n}\n```\n\nThe optional parameters `TupleOverload(Minimum=1, Maximum=21)` determine which overloads are generated.\n\n## Behind the scenes\n\n`TupleOverloadGenerator.Types` adds three methods to tuple, which are ensured for arrays aswell, so that they can be used interchangeably. **If any members on the `params` array are called, except these methods, the generator will fail!**\n\n- `AsSpan(): Span\u003cT\u003e` - Returns the span over the tuple/array\n- `AsRoSpan(): ReadOnlySpan\u003cT\u003e` - Returns the span over the tuple/array\n- `GetPinnableReference(): ref T` - Returns the pinnable reference to the first element in the tuple/array.\n\nThe sourcegenerator `TupleOverloadGenerator` primarly **replaces** the params parameter type with a given tuple type (e.g. `params string[]` -\u003e `(string, string, string)`).\n\n### Tuple as Span\n\n**Tuples cannot be cast to a span can they?**\nNo, they cannot. At least not trivially. To obtain a span from a tuple, we have to cheat, and by cheat I mean unsafe hacks that may not work in the future.\n\nThe source generator adds the following extension methods to the value types with 1-21 parameters:\n\n```csharp\nusing System.Runtime.CompilerServices;\nusing System.Runtime.InteropServices;\n\n[...]\n\n[MethodImpl(MethodImplOptions.AggressiveInlining)]\npublic static ref T GetPinnableReference\u003cT\u003e(in this (T, T) tuple) {\n    return ref Unsafe.AsRef(in tuple).Item1;\n}\n[MethodImpl(MethodImplOptions.AggressiveInlining)]\npublic static Span\u003cT\u003e AsSpan\u003cT\u003e(in this (T, T) tuple) {\n    return MemoryMarshal.CreateSpan(ref tuple.GetPinnableReference(), 2);\n}\n[MethodImpl(MethodImplOptions.AggressiveInlining)]\npublic static ReadOnlySpan\u003cT\u003e AsRoSpan\u003cT\u003e(in this (T, T) tuple) {\n    return MemoryMarshal.CreateReadOnlySpan(ref tuple.GetPinnableReference(), 2);\n}\n\n[...]\n```\n\n### GetPinnableReference\nGetPinnableReference returns a reference to the first element in the tuple, treating it as a inline array.\nThis is unsafe, because RYU may reorder the items, so that the following layout applies:\n\n```js\n[Item2][padding][Item1][padding][Item3][padding]\n```\n\nIf the structure has padding inconsistent with the array allocation padding (which is unlikely, but again undefined), or the structure is reordered this will not work! The runtime the tuntime should not change the ordering, because the element size is equal.\n\nNote that for 9 arguments the following type is created `ValueTuple\u003cT,T,T,T,T,T,T,ValueTuple\u003cT,T\u003e\u003e` here the size of elements is not equal, reordering may occur.\n\n### AsSpan \u0026 AsRoSpan\n`AsSpan` creates a span from the reference to the first element in the tuple with length equal to the number of elements in the tuple.\n\nThe primary issue here is that [MemoryMarshal.CreateSpan](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.memorymarshal.createspan) is not specified to work with a tuple! At some point Microsoft may deicide that this should throw an exception instead of succeeding. We are working with undefined behaviour here!\n\nOther then that the `in` keyword for the parameter too can be a problem. It specifies that the readonly-**reference** to the struct is passed instead of the struct itself. In and of itself this is not a problem, but the memory analyzer will complain when returning the span to a different context.\n\nAll in all, I have tested this with `3.1.423`, `6.0.401` and `7.0.0-rc.2.22472.3` on Linux and with .NET Framework `4.8.1` on Windows. The funcionality is untested on macOS.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprophetlamb%2Ftupleoverloadgenerator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fprophetlamb%2Ftupleoverloadgenerator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprophetlamb%2Ftupleoverloadgenerator/lists"}