├── .github └── workflows │ └── build.yaml ├── .gitignore ├── Directory.Build.props ├── Images ├── Lepracaun.100.png ├── Lepracaun.2000.png ├── Lepracaun.svg ├── README.md └── u1F6B6-walking.svg ├── LICENSE ├── Lepracaun.Tests ├── InvokingTest.cs ├── Lepracaun.Tests.csproj └── ThreadBoundTests.cs ├── Lepracaun.sln ├── Lepracaun ├── Application.cs ├── Internal │ ├── ContinuationInformation.cs │ └── Win32NativeMethods.cs ├── Lepracaun.csproj ├── ManagedThreadSynchronizationContext.cs ├── Properties │ └── AssemblyInfo.cs ├── SingleThreadedSynchronizationContext.cs ├── ThreadBoundSynchronizationContext.cs ├── UnhandledExceptionEventArgs.cs ├── Win32MessagingSynchronizationContext.cs └── WorkerThreadSynchronizationContext.cs ├── NuGet.Config ├── README.md ├── build-nupkg.bat └── build-nupkg.sh /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: windows-latest 8 | #runs-on: [self-hosted, Linux, X64] 9 | steps: 10 | 11 | #----------------------------------------------------------------------- 12 | # Checkout 13 | 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | # lfs: true 18 | # https://stackoverflow.com/questions/61463578/github-actions-actions-checkoutv2-lfs-true-flag-not-converting-pointers-to-act 19 | #- name: Checkout LFS objects 20 | # run: git lfs checkout 21 | 22 | - name: Extract branch name 23 | id: extract_branch_name 24 | run: | 25 | $branch_name=$(git name-rev --name-only --exclude=tags/* HEAD) 26 | echo "Detected current branch: ${branch_name}" 27 | echo "branch_name=${branch_name}" >> $GITHUB_OUTPUT 28 | 29 | #----------------------------------------------------------------------- 30 | # Setup environments 31 | 32 | - name: Setup .NET SDKs 33 | uses: actions/setup-dotnet@v4 34 | with: 35 | dotnet-version: | 36 | 2.2.x 37 | 3.1.x 38 | 5.0.x 39 | 6.0.x 40 | 7.0.x 41 | 8.0.x 42 | 43 | #- name: Setup NuGet package reference 44 | # run: | 45 | # dotnet nuget add source ${{secrets.GH_LOCAL_NUGET_URL}} -n ref1 -u ${{secrets.GH_LOCAL_NUGET_USER}} -p ${{secrets.GH_LOCAL_NUGET_PASSWORD}} --store-password-in-clear-text --configfile nuget.config 46 | # dotnet nuget add source ${{secrets.GH_NUGET_URL}} -n ref2 -u ${{secrets.GH_NUGET_USER}} -p ${{secrets.GH_NUGET_PASSWORD}} --store-password-in-clear-text --configfile nuget.config 47 | 48 | #----------------------------------------------------------------------- 49 | # Build 50 | 51 | - name: Build 52 | run: dotnet build -p:Configuration=Release -p:BuildIdentifier=${GITHUB_RUN_NUMBER} 53 | 54 | - name: Build NuGet packages 55 | run: dotnet pack -p:Configuration=Release -p:BuildIdentifier=${GITHUB_RUN_NUMBER} -o artifacts 56 | 57 | #----------------------------------------------------------------------- 58 | # Test 59 | 60 | - name: Test 61 | run: dotnet test --no-restore --verbosity normal -p:CITest=True 62 | timeout-minutes: 5 63 | 64 | #----------------------------------------------------------------------- 65 | # Deploy packages (develop) 66 | 67 | #- name: Deploy NuGet package (develop/ref1) 68 | # if: startsWith( github.ref, 'refs/tags/' ) 69 | # run: | 70 | # dotnet nuget push artifacts\Lepracaun.*.nupkg --source ref1 71 | 72 | #----------------------------------------------------------------------- 73 | # Deploy packages (main) 74 | 75 | #- name: Deploy NuGet package (main/ref2) 76 | # if: (startsWith( github.ref, 'refs/tags/' )) && (endsWith(steps.extract_branch_name.outputs.branch_name, 'main')) 77 | # run: | 78 | # dotnet nuget push artifacts\Lepracaun.*.nupkg --source ref2 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | 24 | # Visual Studio 2015 cache/options directory 25 | .vs/ 26 | # Uncomment if you have tasks that create the project's static files in wwwroot 27 | #wwwroot/ 28 | 29 | # MSTest test Results 30 | [Tt]est[Rr]esult*/ 31 | [Bb]uild[Ll]og.* 32 | 33 | # NUNIT 34 | *.VisualState.xml 35 | TestResult.xml 36 | 37 | # Build Results of an ATL Project 38 | [Dd]ebugPS/ 39 | [Rr]eleasePS/ 40 | dlldata.c 41 | 42 | # DNX 43 | project.lock.json 44 | artifacts/ 45 | 46 | *_i.c 47 | *_p.c 48 | *_i.h 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.tmp_proj 63 | *.log 64 | *.vspscc 65 | *.vssscc 66 | .builds 67 | *.pidb 68 | *.svclog 69 | *.scc 70 | 71 | # Chutzpah Test files 72 | _Chutzpah* 73 | 74 | # Visual C++ cache files 75 | ipch/ 76 | *.aps 77 | *.ncb 78 | *.opendb 79 | *.opensdf 80 | *.sdf 81 | *.cachefile 82 | 83 | # Visual Studio profiler 84 | *.psess 85 | *.vsp 86 | *.vspx 87 | *.sap 88 | 89 | # TFS 2012 Local Workspace 90 | $tf/ 91 | 92 | # Guidance Automation Toolkit 93 | *.gpState 94 | 95 | # ReSharper is a .NET coding add-in 96 | _ReSharper*/ 97 | *.[Rr]e[Ss]harper 98 | *.DotSettings.user 99 | 100 | # JustCode is a .NET coding add-in 101 | .JustCode 102 | 103 | # TeamCity is a build add-in 104 | _TeamCity* 105 | 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | 109 | # NCrunch 110 | _NCrunch_* 111 | .*crunch*.local.xml 112 | nCrunchTemp_* 113 | 114 | # MightyMoose 115 | *.mm.* 116 | AutoTest.Net/ 117 | 118 | # Web workbench (sass) 119 | .sass-cache/ 120 | 121 | # Installshield output folder 122 | [Ee]xpress/ 123 | 124 | # DocProject is a documentation generator add-in 125 | DocProject/buildhelp/ 126 | DocProject/Help/*.HxT 127 | DocProject/Help/*.HxC 128 | DocProject/Help/*.hhc 129 | DocProject/Help/*.hhk 130 | DocProject/Help/*.hhp 131 | DocProject/Help/Html2 132 | DocProject/Help/html 133 | 134 | # Click-Once directory 135 | publish/ 136 | 137 | # Publish Web Output 138 | *.[Pp]ublish.xml 139 | *.azurePubxml 140 | # TODO: Comment the next line if you want to checkin your web deploy settings 141 | # but database connection strings (with potential passwords) will be unencrypted 142 | *.pubxml 143 | *.publishproj 144 | 145 | # NuGet Packages 146 | *.nupkg 147 | # The packages folder can be ignored because of Package Restore 148 | **/packages/* 149 | # except build/, which is used as an MSBuild target. 150 | !**/packages/build/ 151 | # Uncomment if necessary however generally it will be regenerated when needed 152 | #!**/packages/repositories.config 153 | 154 | # Microsoft Azure Build Output 155 | csx/ 156 | *.build.csdef 157 | 158 | # Microsoft Azure Emulator 159 | ecf/ 160 | rcf/ 161 | 162 | # Microsoft Azure ApplicationInsights config file 163 | ApplicationInsights.config 164 | 165 | # Windows Store app package directory 166 | AppPackages/ 167 | BundleArtifacts/ 168 | 169 | # Visual Studio cache files 170 | # files ending in .cache can be ignored 171 | *.[Cc]ache 172 | # but keep track of directories ending in .cache 173 | !*.[Cc]ache/ 174 | 175 | # Others 176 | ClientBin/ 177 | ~$* 178 | *~ 179 | *.dbmdl 180 | *.dbproj.schemaview 181 | *.pfx 182 | *.publishsettings 183 | node_modules/ 184 | orleans.codegen.cs 185 | 186 | # RIA/Silverlight projects 187 | Generated_Code/ 188 | 189 | # Backup & report files from converting an old project file 190 | # to a newer Visual Studio version. Backup files are not needed, 191 | # because we have git ;-) 192 | _UpgradeReport_Files/ 193 | Backup*/ 194 | UpgradeLog*.XML 195 | UpgradeLog*.htm 196 | 197 | # SQL Server files 198 | *.mdf 199 | *.ldf 200 | 201 | # Business Intelligence projects 202 | *.rdl.data 203 | *.bim.layout 204 | *.bim_*.settings 205 | 206 | # Microsoft Fakes 207 | FakesAssemblies/ 208 | 209 | # GhostDoc plugin setting file 210 | *.GhostDoc.xml 211 | 212 | # Node.js Tools for Visual Studio 213 | .ntvs_analysis.dat 214 | 215 | # Visual Studio 6 build log 216 | *.plg 217 | 218 | # Visual Studio 6 workspace options file 219 | *.opt 220 | 221 | # Visual Studio LightSwitch build output 222 | **/*.HTMLClient/GeneratedArtifacts 223 | **/*.DesktopClient/GeneratedArtifacts 224 | **/*.DesktopClient/ModelManifest.xml 225 | **/*.Server/GeneratedArtifacts 226 | **/*.Server/ModelManifest.xml 227 | _Pvt_Extensions 228 | 229 | # Paket dependency manager 230 | .paket/paket.exe 231 | 232 | # FAKE - F# Make 233 | .fake/ 234 | 235 | *.VC.db 236 | 237 | .idea/ 238 | 239 | test-templates/ 240 | 241 | *.received.* 242 | 243 | *.bak 244 | *.orig 245 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | latest 5 | enable 6 | AnyCPU 7 | 8 | true 9 | true 10 | false 11 | true 12 | false 13 | true 14 | git 15 | https://github.com/kekyo/Lepracaun.git 16 | 17 | Lepracaun 18 | false 19 | true 20 | $(NoWarn);CS1570;CS1591;CA1416;CS0436 21 | 22 | Lepracaun 23 | Lepracaun 24 | Copyright (c) Kouji Matsui 25 | Varies of .NET Synchronization Context. 26 | 27 | Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 28 | Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 29 | Apache-2.0 30 | https://github.com/kekyo/Lepracaun 31 | Lepracaun.100.png 32 | synchronization;context;threading;apartment;affinity 33 | .pdb 34 | false 35 | $(NoWarn);NU1605;NU1701;NU1803;NU1902;NU1903 36 | 37 | 38 | 39 | portable 40 | false 41 | false 42 | false 43 | 44 | 45 | 46 | embedded 47 | true 48 | true 49 | true 50 | $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\')) 51 | $(RepoRoot)=. 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Images/Lepracaun.100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kekyo/Lepracaun/02b60fa23e09b89a15eebfa41bec071c12d9e984/Images/Lepracaun.100.png -------------------------------------------------------------------------------- /Images/Lepracaun.2000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kekyo/Lepracaun/02b60fa23e09b89a15eebfa41bec071c12d9e984/Images/Lepracaun.2000.png -------------------------------------------------------------------------------- /Images/Lepracaun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 112 | -------------------------------------------------------------------------------- /Images/README.md: -------------------------------------------------------------------------------- 1 | The icon images imported from [FireFox emoji](https://github.com/mozilla/fxemoji). 2 | These items exclude FlashCap project copyright notice. 3 | 4 | ---- 5 | 6 | ## License for the Code 7 | 8 | Copyright 2015, Mozilla Foundation 9 | 10 | Licensed under the Apache License, Version 2.0 (the "License"); 11 | you may not use this file except in compliance with the License. 12 | You may obtain a copy of the License at 13 | 14 | http://www.apache.org/licenses/LICENSE-2.0 15 | 16 | Unless required by applicable law or agreed to in writing, software 17 | distributed under the License is distributed on an "AS IS" BASIS, 18 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | See the License for the specific language governing permissions and 20 | limitations under the License. 21 | 22 | 23 | 24 | ## License for the Visual Design 25 | 26 | ### Creative Commons Attribution 4.0 International (CC BY 4.0) 27 | https://creativecommons.org/licenses/by/4.0/legalcode 28 | or for the human readable summary: https://creativecommons.org/licenses/by/4.0/ 29 | 30 | 31 | #### You are free to: 32 | **Share** — copy and redistribute the material in any medium or format 33 | **Adapt** — remix, transform, and build upon the material for any purpose, even commercially. 34 | The licensor cannot revoke these freedoms as long as you follow the license terms. 35 | 36 | 37 | #### Under the following terms: 38 | **Attribution** — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. 39 | No additional restrictions — You may not apply legal terms or **technological measures** that legally restrict others from doing anything the license permits. 40 | 41 | #### Notices: 42 | You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation. No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material. 43 | -------------------------------------------------------------------------------- /Images/u1F6B6-walking.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 17 | 29 | 34 | 40 | 50 | 58 | 60 | 63 | 65 | 69 | 71 | 73 | 76 | 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /Lepracaun.Tests/InvokingTest.cs: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Lepracaun - Varies of .NET Synchronization Context. 4 | // Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | // 6 | // Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | // 8 | ///////////////////////////////////////////////////////////////////////////////////// 9 | 10 | using NUnit.Framework; 11 | using System; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | 15 | using static NUnit.Framework.Assert; 16 | 17 | namespace Lepracaun; 18 | 19 | [TestFixture] 20 | public sealed class InvokingTests 21 | { 22 | private static async Task TestBodyAsync() 23 | { 24 | await Application.Current.InvokeAsync(() => 25 | { 26 | Thread.Sleep(100); 27 | }); 28 | 29 | var result1 = await Application.Current.InvokeAsync(() => 30 | { 31 | Thread.Sleep(100); 32 | return 123; 33 | }); 34 | 35 | AreEqual(123, result1); 36 | 37 | await Application.Current.InvokeAsync(async () => 38 | { 39 | await Task.Delay(100); 40 | }); 41 | 42 | var result2 = await Application.Current.InvokeAsync(async () => 43 | { 44 | await Task.Delay(100); 45 | return 456; 46 | }); 47 | 48 | AreEqual(456, result2); 49 | } 50 | 51 | [Test] 52 | public void RunTest1() 53 | { 54 | IsNull(SynchronizationContext.Current); 55 | 56 | using var app = new Application(); 57 | 58 | app.Run(TestBodyAsync()); 59 | } 60 | 61 | [Test] 62 | public void RunTest2() 63 | { 64 | IsNull(SynchronizationContext.Current); 65 | 66 | using var app = new Application(); 67 | 68 | app.Run(() => TestBodyAsync()); 69 | } 70 | 71 | [Test] 72 | public void RunManyInvoking() 73 | { 74 | IsNull(SynchronizationContext.Current); 75 | 76 | using var app = new Application(); 77 | var id = app.BoundIdentity; 78 | 79 | app.Run(async () => 80 | { 81 | for (var index = 0; index < 1000000; index++) 82 | { 83 | await Task.Yield(); 84 | 85 | AreEqual(id, Thread.CurrentThread.ManagedThreadId); 86 | } 87 | }); 88 | } 89 | 90 | [Test] 91 | public void RunExceptionTest1() 92 | { 93 | IsNull(SynchronizationContext.Current); 94 | 95 | using var app = new Application(); 96 | 97 | Exception? ex = null; 98 | app.UnhandledException += (s, e) => 99 | { 100 | ex = e.Exception; 101 | e.Handled = true; 102 | }; 103 | 104 | app.Run(() => 105 | { 106 | var tcs = new TaskCompletionSource(); 107 | tcs.SetException(new ApplicationException("ABC")); 108 | return tcs.Task; 109 | }); 110 | 111 | IsTrue(ex is ApplicationException aex && aex.Message == "ABC"); 112 | } 113 | 114 | [Test] 115 | public void RunExceptionTest2() 116 | { 117 | IsNull(SynchronizationContext.Current); 118 | 119 | using var app = new Application(); 120 | 121 | Exception? ex = null; 122 | app.UnhandledException += (s, e) => 123 | { 124 | ex = e.Exception; 125 | e.Handled = true; 126 | }; 127 | 128 | app.Run(async () => 129 | { 130 | await Task.Delay(100); 131 | throw new ApplicationException("ABC"); 132 | }); 133 | 134 | IsTrue(ex is ApplicationException aex && aex.Message == "ABC"); 135 | } 136 | 137 | [Test] 138 | public void RunExceptionTest3() 139 | { 140 | IsNull(SynchronizationContext.Current); 141 | 142 | using var app = new Application(); 143 | 144 | Exception? ex = null; 145 | app.UnhandledException += (s, e) => 146 | { 147 | ex = e.Exception; 148 | e.Handled = true; 149 | }; 150 | 151 | app.Run(async () => 152 | { 153 | static async Task Child() 154 | { 155 | await Task.Delay(100); 156 | throw new ApplicationException("ABC"); 157 | } 158 | 159 | await Task.Delay(100); 160 | await Child(); 161 | }); 162 | 163 | IsTrue(ex is ApplicationException aex && aex.Message == "ABC"); 164 | } 165 | 166 | [Test] 167 | public void RunExceptionTest4() 168 | { 169 | IsNull(SynchronizationContext.Current); 170 | 171 | using var app = new Application(); 172 | 173 | Exception? ex = null; 174 | try 175 | { 176 | app.Run(() => 177 | { 178 | throw new ApplicationException("ABC"); 179 | }); 180 | } 181 | catch (Exception ex2) 182 | { 183 | ex = ex2; 184 | } 185 | 186 | IsTrue(ex is ApplicationException aex && aex.Message == "ABC"); 187 | } 188 | 189 | [Test] 190 | public void RunExceptionTest5() 191 | { 192 | IsNull(SynchronizationContext.Current); 193 | 194 | using var app = new Application(); 195 | 196 | Exception? ex = null; 197 | try 198 | { 199 | app.Run(() => 200 | { 201 | var tcs = new TaskCompletionSource(); 202 | tcs.SetException(new ApplicationException("ABC")); 203 | return tcs.Task; 204 | }); 205 | } 206 | catch (Exception ex2) 207 | { 208 | ex = ex2; 209 | } 210 | 211 | IsTrue(ex is ApplicationException aex && aex.Message == "ABC"); 212 | } 213 | 214 | [Test] 215 | public void RunExceptionTest6() 216 | { 217 | IsNull(SynchronizationContext.Current); 218 | 219 | using var app = new Application(); 220 | 221 | Exception? ex = null; 222 | try 223 | { 224 | app.Run(async() => 225 | { 226 | await Task.Delay(1); 227 | throw new ApplicationException("ABC"); 228 | }); 229 | } 230 | catch (Exception ex2) 231 | { 232 | ex = ex2; 233 | } 234 | 235 | IsTrue(ex is ApplicationException aex && aex.Message == "ABC"); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Lepracaun.Tests/Lepracaun.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net48;net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Lepracaun.Tests/ThreadBoundTests.cs: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Lepracaun - Varies of .NET Synchronization Context. 4 | // Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | // 6 | // Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | // 8 | ///////////////////////////////////////////////////////////////////////////////////// 9 | 10 | using Lepracaun.Internal; 11 | using NUnit.Framework; 12 | using System; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | 16 | using static NUnit.Framework.Assert; 17 | 18 | namespace Lepracaun; 19 | 20 | [TestFixture] 21 | public sealed class ThreadBoundTests 22 | { 23 | private static async Task TestBodyAsync(int threadId, Func getter) 24 | { 25 | AreEqual(threadId, getter()); 26 | 27 | await Task.Delay(100); 28 | 29 | AreEqual(threadId, getter()); 30 | 31 | await Task.Delay(100); 32 | 33 | AreEqual(threadId, getter()); 34 | } 35 | 36 | [Test] 37 | public void ThreadBoundTest() 38 | { 39 | IsNull(SynchronizationContext.Current); 40 | 41 | using var app = new Application(); 42 | 43 | app.Run(TestBodyAsync(app.BoundIdentity, () => Thread.CurrentThread.ManagedThreadId)); 44 | } 45 | 46 | [Test] 47 | public void Win32ThreadBoundTest() 48 | { 49 | IsNull(SynchronizationContext.Current); 50 | 51 | using var app = new Application( 52 | new Win32MessagingSynchronizationContext()); 53 | 54 | app.Run(TestBodyAsync(app.BoundIdentity, () => Win32NativeMethods.GetCurrentThreadId())); 55 | } 56 | 57 | private static async Task WorkerTestBodyAsync(int threadId) 58 | { 59 | AreNotEqual(threadId, Thread.CurrentThread.ManagedThreadId); 60 | 61 | await Task.Delay(100); 62 | 63 | AreEqual(threadId, Thread.CurrentThread.ManagedThreadId); 64 | 65 | await Task.Delay(100); 66 | 67 | AreEqual(threadId, Thread.CurrentThread.ManagedThreadId); 68 | } 69 | 70 | [Test] 71 | public async Task WorkerThreadBoundTest() 72 | { 73 | IsNull(SynchronizationContext.Current); 74 | 75 | using var app = new Application( 76 | new WorkerThreadSynchronizationContext()); 77 | 78 | var task = WorkerTestBodyAsync( 79 | app.BoundIdentity); 80 | 81 | app.Run(task); 82 | 83 | await task; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Lepracaun.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32414.318 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DB1A02F9-B9D0-4FCA-BE56-E582B399272D}" 7 | ProjectSection(SolutionItems) = preProject 8 | .gitignore = .gitignore 9 | build-nupkg.bat = build-nupkg.bat 10 | build-nupkg.sh = build-nupkg.sh 11 | .github\workflows\build.yaml = .github\workflows\build.yaml 12 | Directory.Build.props = Directory.Build.props 13 | LICENSE = LICENSE 14 | NuGet.Config = NuGet.Config 15 | README.md = README.md 16 | EndProjectSection 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lepracaun", "Lepracaun\Lepracaun.csproj", "{D16EFA11-DECC-45F3-8FA1-1E27FB740E7C}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lepracaun.Tests", "Lepracaun.Tests\Lepracaun.Tests.csproj", "{211F15CC-7819-486F-A5DB-233935A2C8D2}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {D16EFA11-DECC-45F3-8FA1-1E27FB740E7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {D16EFA11-DECC-45F3-8FA1-1E27FB740E7C}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {D16EFA11-DECC-45F3-8FA1-1E27FB740E7C}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {D16EFA11-DECC-45F3-8FA1-1E27FB740E7C}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {211F15CC-7819-486F-A5DB-233935A2C8D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {211F15CC-7819-486F-A5DB-233935A2C8D2}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {211F15CC-7819-486F-A5DB-233935A2C8D2}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {211F15CC-7819-486F-A5DB-233935A2C8D2}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {3F5AF3C2-2CAC-40BB-9AC9-924698AD329F} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /Lepracaun/Application.cs: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Lepracaun - Varies of .NET Synchronization Context. 4 | // Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | // 6 | // Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | // 8 | ///////////////////////////////////////////////////////////////////////////////////// 9 | 10 | using System; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | 14 | namespace Lepracaun; 15 | 16 | /// 17 | /// Pseudo Application class. 18 | /// 19 | public class Application : IDisposable 20 | { 21 | private static readonly object locker = new(); 22 | private static Application? current; 23 | 24 | /// 25 | /// Get current Application instance. 26 | /// 27 | public static Application Current 28 | { 29 | get 30 | { 31 | lock (locker) 32 | { 33 | if (current == null) 34 | { 35 | current = new Application(); 36 | } 37 | } 38 | return current; 39 | } 40 | private set 41 | { 42 | lock (locker) 43 | { 44 | if (!object.ReferenceEquals(value, current)) 45 | { 46 | if (current != null) 47 | { 48 | current.Dispose(); 49 | } 50 | current = value; 51 | } 52 | } 53 | } 54 | } 55 | 56 | private readonly ThreadBoundSynchronizationContext context; 57 | 58 | /// 59 | /// Constructor. 60 | /// 61 | public Application() : 62 | this(new SingleThreadedSynchronizationContext()) 63 | { 64 | } 65 | 66 | /// 67 | /// Constructor. 68 | /// 69 | /// Applicated synchronization context 70 | public Application(ThreadBoundSynchronizationContext context) 71 | { 72 | this.context = context; 73 | this.context.UnhandledException += this.OnUnhandledException!; 74 | 75 | Current = this; 76 | SynchronizationContext.SetSynchronizationContext( 77 | (SynchronizationContext)this.context); 78 | } 79 | 80 | private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) => 81 | this.UnhandledException?.Invoke(this, e); 82 | 83 | /// 84 | /// Dispose. 85 | /// 86 | public void Dispose() 87 | { 88 | this.context.Shutdown(); 89 | this.context.UnhandledException -= this.OnUnhandledException!; 90 | } 91 | 92 | /// 93 | /// Get bound thread identity. 94 | /// 95 | public int BoundIdentity => 96 | this.context.BoundIdentity; 97 | 98 | /// 99 | /// Check current context is bound this. 100 | /// 101 | /// True if bound this. 102 | public bool CheckAccess() => 103 | this.context.CheckAccess(); 104 | 105 | /// 106 | /// Occurred unhandled exception event. 107 | /// 108 | public event EventHandler? UnhandledException; 109 | 110 | /// 111 | /// Run the application. 112 | /// 113 | public void Run() => 114 | this.context.Run(null!); 115 | 116 | /// 117 | /// Run the application with first time task. 118 | /// 119 | /// Bound main task 120 | public void Run(Task mainTask) => 121 | this.context.Run(mainTask); 122 | 123 | /// 124 | /// Run the application with first time task. 125 | /// 126 | /// Bound main action 127 | public void Run(Func mainAction) => 128 | this.context.Run(mainAction()); 129 | 130 | /// 131 | /// Request shutdown. 132 | /// 133 | public void Shutdown() => 134 | this.context.Shutdown(); 135 | 136 | /// 137 | /// Synchronized invoking a delegate on the application context. 138 | /// 139 | /// Action delegate 140 | public void Invoke(Action action) => 141 | this.context.Send(_ => action(), null); 142 | 143 | /// 144 | /// Synchronized invoking a delegate on the application context. 145 | /// 146 | /// Return value type 147 | /// Action delegate 148 | /// Return value 149 | public TResult Invoke(Func action) 150 | { 151 | TResult result = default!; 152 | this.context.Send(_ => result = action(), null); 153 | return result; 154 | } 155 | 156 | /// 157 | /// Asynchronized invoking a delegate on the application context. 158 | /// 159 | /// Action delegate 160 | public void BeginInvoke(Action action) => 161 | this.context.Post(_ => action(), null); 162 | 163 | /// 164 | /// Asynchronized invoking a delegate on the application context. 165 | /// 166 | /// Action delegate 167 | public Task InvokeAsync(Action action) 168 | { 169 | var tcs = new TaskCompletionSource(); 170 | this.context.Post(_ => 171 | { 172 | try 173 | { 174 | action(); 175 | tcs.TrySetResult(0); 176 | } 177 | catch (Exception ex) 178 | { 179 | tcs.TrySetException(ex); 180 | } 181 | }, null); 182 | return tcs.Task; 183 | } 184 | 185 | /// 186 | /// Asynchronized invoking a delegate on the application context. 187 | /// 188 | /// Return value type 189 | /// Action delegate 190 | /// Return value 191 | public Task InvokeAsync(Func action) 192 | { 193 | var tcs = new TaskCompletionSource(); 194 | this.context.Post(_ => 195 | { 196 | try 197 | { 198 | tcs.TrySetResult(action()); 199 | } 200 | catch (Exception ex) 201 | { 202 | tcs.TrySetException(ex); 203 | } 204 | }, null); 205 | return tcs.Task; 206 | } 207 | 208 | /// 209 | /// Asynchronized invoking a delegate on the application context. 210 | /// 211 | /// Asynchronous action delegate 212 | public Task InvokeAsync(Func action) 213 | { 214 | var tcs = new TaskCompletionSource(); 215 | this.context.Post(async _ => 216 | { 217 | try 218 | { 219 | await action().ConfigureAwait(false); 220 | tcs.TrySetResult(0); 221 | } 222 | catch (Exception ex) 223 | { 224 | tcs.TrySetException(ex); 225 | } 226 | }, null); 227 | return tcs.Task; 228 | } 229 | 230 | /// 231 | /// Asynchronized invoking a delegate on the application context. 232 | /// 233 | /// Return value type 234 | /// Asynchronous action delegate 235 | /// Return value 236 | public Task InvokeAsync(Func> action) 237 | { 238 | var tcs = new TaskCompletionSource(); 239 | this.context.Post(async _ => 240 | { 241 | try 242 | { 243 | tcs.TrySetResult(await action().ConfigureAwait(false)); 244 | } 245 | catch (Exception ex) 246 | { 247 | tcs.TrySetException(ex); 248 | } 249 | }, null); 250 | return tcs.Task; 251 | } 252 | } 253 | 254 | /// 255 | /// Pseudo Application class. 256 | /// 257 | /// Applicated synchronization context type 258 | public sealed class Application : Application 259 | where TSynchronizationContext : ThreadBoundSynchronizationContext, new() 260 | { 261 | /// 262 | /// Constructor. 263 | /// 264 | public Application() : 265 | base(new TSynchronizationContext()) 266 | { 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /Lepracaun/Internal/ContinuationInformation.cs: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Lepracaun - Varies of .NET Synchronization Context. 4 | // Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | // 6 | // Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | // 8 | ///////////////////////////////////////////////////////////////////////////////////// 9 | 10 | using System.Threading; 11 | 12 | namespace Lepracaun.Internal; 13 | 14 | internal struct ContinuationInformation 15 | { 16 | public SendOrPostCallback Continuation; 17 | public object? State; 18 | } 19 | -------------------------------------------------------------------------------- /Lepracaun/Internal/Win32NativeMethods.cs: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Lepracaun - Varies of .NET Synchronization Context. 4 | // Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | // 6 | // Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | // 8 | ///////////////////////////////////////////////////////////////////////////////////// 9 | 10 | using System; 11 | using System.Runtime.InteropServices; 12 | 13 | namespace Lepracaun.Internal; 14 | 15 | internal static class Win32NativeMethods 16 | { 17 | public static readonly int WM_QUIT = 0x0012; 18 | 19 | public struct MSG 20 | { 21 | public IntPtr hWnd; 22 | public int msg; 23 | public IntPtr wParam; 24 | public IntPtr lParam; 25 | public IntPtr result; 26 | } 27 | 28 | [DllImport("user32.dll", SetLastError = true)] 29 | public static extern bool PostThreadMessage(int threadId, int msg, IntPtr wParam, IntPtr lParam); 30 | 31 | [DllImport("user32.dll", SetLastError = true)] 32 | public static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, int wMsgFilterMin, int wMsgFilterMax); 33 | 34 | [DllImport("user32.dll", SetLastError = true)] 35 | public static extern bool TranslateMessage([In] ref MSG lpMsg); 36 | 37 | [DllImport("user32.dll", SetLastError = true)] 38 | public static extern IntPtr DispatchMessage([In] ref MSG lpmsg); 39 | 40 | [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] 41 | public static extern int RegisterWindowMessageW(string lpString); 42 | 43 | [DllImport("kernel32.dll")] 44 | public static extern int GetCurrentThreadId(); 45 | } 46 | -------------------------------------------------------------------------------- /Lepracaun/Lepracaun.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net35;net40;net45;net461;net48;net481;netstandard1.3;netstandard1.6;netstandard2.0;netstandard2.1;netcoreapp2.0;netcoreapp2.2;netcoreapp3.0;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Lepracaun/ManagedThreadSynchronizationContext.cs: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Lepracaun - Varies of .NET Synchronization Context. 4 | // Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | // 6 | // Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | // 8 | ///////////////////////////////////////////////////////////////////////////////////// 9 | 10 | using System; 11 | using Lepracaun.Internal; 12 | using System.Collections.Generic; 13 | using System.Reflection; 14 | using System.Threading; 15 | 16 | #if !NET35 && !NET40 17 | using System.Runtime.ExceptionServices; 18 | #endif 19 | 20 | namespace Lepracaun; 21 | 22 | /// 23 | /// Custom synchronization context implementation using managed thread. 24 | /// 25 | public abstract class ManagedThreadSynchronizationContext : 26 | ThreadBoundSynchronizationContext 27 | { 28 | /// 29 | /// Continuation queue. 30 | /// 31 | private readonly Queue queue = new(); 32 | 33 | /// 34 | /// Flag for continuation availability. 35 | /// 36 | private readonly ManualResetEventSlim available = new(); 37 | 38 | /// 39 | /// Flag for finalized. 40 | /// 41 | private bool completeAdding; 42 | 43 | /// 44 | /// Finalized with an exception. 45 | /// 46 | private Exception? completeWithException; 47 | 48 | /// 49 | /// Constructor. 50 | /// 51 | protected ManagedThreadSynchronizationContext() 52 | { 53 | } 54 | 55 | protected override sealed int GetCurrentThreadId() => 56 | Thread.CurrentThread.ManagedThreadId; 57 | 58 | protected override sealed void OnPost( 59 | int targetThreadId, SendOrPostCallback continuation, object? state) 60 | { 61 | // Add continuation information into queue. 62 | var entry = new ContinuationInformation 63 | { Continuation = continuation, State = state }; 64 | lock (this.queue) 65 | { 66 | this.queue.Enqueue(entry); 67 | if (this.queue.Count == 1) 68 | { 69 | this.available.Set(); 70 | } 71 | } 72 | } 73 | 74 | protected override sealed void OnRun( 75 | int targetThreadId) 76 | { 77 | // Run queue consumer. 78 | while (true) 79 | { 80 | this.available.Wait(); 81 | 82 | ContinuationInformation entry; 83 | lock (this.queue) 84 | { 85 | if (this.queue.Count == 0) 86 | { 87 | this.available.Reset(); 88 | if (this.completeAdding) 89 | { 90 | break; 91 | } 92 | else 93 | { 94 | continue; 95 | } 96 | } 97 | entry = this.queue.Dequeue(); 98 | } 99 | 100 | // Invoke continuation. 101 | this.OnInvoke( 102 | entry.Continuation, 103 | entry.State); 104 | } 105 | 106 | if (this.completeWithException is { } ex) 107 | { 108 | #if NET35 || NET40 109 | throw new TargetInvocationException(ex); 110 | #else 111 | var edi = ExceptionDispatchInfo.Capture(ex); 112 | edi.Throw(); 113 | #endif 114 | } 115 | } 116 | 117 | protected override sealed void OnShutdown( 118 | int targetThreadId, Exception? ex) 119 | { 120 | this.completeWithException = ex; 121 | this.completeAdding = true; 122 | this.available.Set(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Lepracaun/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Lepracaun - Varies of .NET Synchronization Context. 4 | // Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | // 6 | // Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | // 8 | ///////////////////////////////////////////////////////////////////////////////////// 9 | 10 | using System.Runtime.CompilerServices; 11 | 12 | [assembly: InternalsVisibleTo("Lepracaun.Tests")] 13 | -------------------------------------------------------------------------------- /Lepracaun/SingleThreadedSynchronizationContext.cs: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Lepracaun - Varies of .NET Synchronization Context. 4 | // Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | // 6 | // Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | // 8 | ///////////////////////////////////////////////////////////////////////////////////// 9 | 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace Lepracaun; 14 | 15 | /// 16 | /// Custom synchronization context implementation running on current thread. 17 | /// 18 | public sealed class SingleThreadedSynchronizationContext : 19 | ManagedThreadSynchronizationContext 20 | { 21 | /// 22 | /// Constructor. 23 | /// 24 | public SingleThreadedSynchronizationContext() => 25 | this.SetTargetThreadId(Thread.CurrentThread.ManagedThreadId); 26 | 27 | /// 28 | /// Copy this context. 29 | /// 30 | /// Copied context. 31 | public override SynchronizationContext CreateCopy() => 32 | new SingleThreadedSynchronizationContext(); 33 | } 34 | -------------------------------------------------------------------------------- /Lepracaun/ThreadBoundSynchronizationContext.cs: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Lepracaun - Varies of .NET Synchronization Context. 4 | // Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | // 6 | // Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | // 8 | ///////////////////////////////////////////////////////////////////////////////////// 9 | 10 | using System; 11 | using System.Diagnostics; 12 | using System.Reflection; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | 16 | #if !NET35 && !NET40 17 | using System.Runtime.ExceptionServices; 18 | #endif 19 | 20 | #pragma warning disable CS0618 21 | 22 | namespace Lepracaun; 23 | 24 | /// 25 | /// Leprecaun central synchronization context. 26 | /// 27 | public abstract class ThreadBoundSynchronizationContext : 28 | SynchronizationContext 29 | { 30 | /// 31 | /// This synchronization context bound thread id. 32 | /// 33 | private int boundThreadId; 34 | 35 | /// 36 | /// Number of recursive posts. 37 | /// 38 | private int recursiveCount = 0; 39 | 40 | /// 41 | /// Constructor. 42 | /// 43 | protected ThreadBoundSynchronizationContext() => 44 | this.boundThreadId = -1; 45 | 46 | protected void SetTargetThreadId(int targetThreadId) 47 | { 48 | if (Interlocked.CompareExchange( 49 | ref this.boundThreadId, targetThreadId, -1) != -1) 50 | { 51 | throw new InvalidOperationException(); 52 | } 53 | } 54 | 55 | /// 56 | /// Get bound thread identity. 57 | /// 58 | public int BoundIdentity => 59 | this.boundThreadId; 60 | 61 | /// 62 | /// Occurred unhandled exception event. 63 | /// 64 | public event EventHandler? UnhandledException; 65 | 66 | /// 67 | /// Check current context is bound this. 68 | /// 69 | /// True if bound this. 70 | public bool CheckAccess() => 71 | this.GetCurrentThreadId() == this.boundThreadId; 72 | 73 | /// 74 | /// Get current thread identity. 75 | /// 76 | /// Thread identity 77 | protected abstract int GetCurrentThreadId(); 78 | 79 | /// 80 | /// Post continuation into synchronization context. 81 | /// 82 | /// Target thread identity. 83 | /// Continuation callback delegate. 84 | /// Continuation argument. 85 | protected abstract void OnPost( 86 | int targetThreadId, SendOrPostCallback continuation, object? state); 87 | 88 | /// 89 | /// Execute message queue. 90 | /// 91 | /// Target thread identity. 92 | protected abstract void OnRun( 93 | int targetThreadId); 94 | 95 | /// 96 | /// Shutdown requested. 97 | /// 98 | /// Target thread identity. 99 | [Obsolete("Use OnShutdown with exception overload instead.")] 100 | protected virtual void OnShutdown( 101 | int targetThreadId) => 102 | this.OnShutdown(targetThreadId, null); 103 | 104 | /// 105 | /// Shutdown requested. 106 | /// 107 | /// Target thread identity. 108 | /// Exception when shutdown reason 109 | protected abstract void OnShutdown( 110 | int targetThreadId, Exception? ex); 111 | 112 | private Exception UnwrapIfRequired(Exception ex) => 113 | ex switch 114 | { 115 | AggregateException aex when aex.InnerExceptions.Count == 1 => aex.InnerExceptions[0], 116 | TargetInvocationException tex when tex.InnerException != null => tex.InnerException, 117 | _ => ex, 118 | }; 119 | 120 | private bool InvokeUnhandledException(Exception ex) 121 | { 122 | var e = new UnhandledExceptionEventArgs(ex); 123 | 124 | try 125 | { 126 | this.UnhandledException?.Invoke(this, e); 127 | } 128 | catch (Exception ex2) 129 | { 130 | Trace.WriteLine(ex2.ToString()); 131 | } 132 | 133 | return e.Handled; 134 | } 135 | 136 | protected void OnInvoke(SendOrPostCallback continuation, object? state) 137 | { 138 | try 139 | { 140 | continuation(state); 141 | } 142 | catch (Exception ex) 143 | { 144 | if (!this.InvokeUnhandledException(UnwrapIfRequired(ex))) 145 | { 146 | throw; 147 | } 148 | } 149 | } 150 | 151 | /// 152 | /// Send continuation into synchronization context. 153 | /// 154 | /// Continuation callback delegate. 155 | /// Continuation argument. 156 | public override void Send(SendOrPostCallback continuation, object? state) 157 | { 158 | // If current thread id is target thread id: 159 | var currentThreadId = this.GetCurrentThreadId(); 160 | if (currentThreadId == this.boundThreadId) 161 | { 162 | // Invoke continuation on current thread. 163 | this.OnInvoke(continuation, state); 164 | return; 165 | } 166 | 167 | using var mre = new ManualResetEventSlim(false); 168 | #if NET35 || NET40 169 | Exception? edi = null; 170 | #else 171 | ExceptionDispatchInfo? edi = null; 172 | #endif 173 | 174 | // Marshal to. 175 | this.OnPost(this.boundThreadId, _ => 176 | { 177 | try 178 | { 179 | this.OnInvoke(continuation, state); 180 | } 181 | catch (Exception ex) 182 | { 183 | #if NET35 || NET40 184 | edi = ex; 185 | #else 186 | edi = ExceptionDispatchInfo.Capture(ex); 187 | #endif 188 | } 189 | finally 190 | { 191 | mre.Set(); 192 | } 193 | }, null); 194 | 195 | // Yes, this can easily cause a deadlock on UI threads: 196 | // * But as long as the Send() method requires blocking, we have no choice. 197 | // * If this context is bound to the UI thread itself, 198 | // it is no problem, as it will be bypassed by the if block above. 199 | mre.Wait(); 200 | 201 | if (edi != null) 202 | { 203 | #if NET35 || NET40 204 | throw new TargetInvocationException(edi); 205 | #else 206 | edi.Throw(); 207 | #endif 208 | } 209 | } 210 | 211 | /// 212 | /// Post continuation into synchronization context. 213 | /// 214 | /// Continuation callback delegate. 215 | /// Continuation argument. 216 | public override void Post(SendOrPostCallback continuation, object? state) 217 | { 218 | // If current thread id is target thread id: 219 | var currentThreadId = this.GetCurrentThreadId(); 220 | if (currentThreadId == this.boundThreadId) 221 | { 222 | // HACK: If current thread is already target thread, invoke continuation directly. 223 | // But if continuation has invokeing Post/Send recursive, cause stack overflow. 224 | // We can fix this problem by simple solution: Continuation invoke every post into queue, 225 | // but performance will be lost. 226 | // This counter uses post for scattering (each 50 times). 227 | if (recursiveCount < 50) 228 | { 229 | recursiveCount++; 230 | try 231 | { 232 | // Invoke continuation on current thread is better performance. 233 | this.OnInvoke(continuation, state); 234 | } 235 | finally 236 | { 237 | recursiveCount--; 238 | } 239 | return; 240 | } 241 | } 242 | 243 | this.OnPost(this.boundThreadId, continuation, state); 244 | } 245 | 246 | /// 247 | /// Schedule task final completion. 248 | /// 249 | /// Task 250 | protected void HookTaskFinalizer(Task? task) => 251 | task?.ContinueWith(t => 252 | { 253 | if (t.IsCanceled) 254 | { 255 | var ex = UnwrapIfRequired(t.Exception!); 256 | if (!this.InvokeUnhandledException(ex)) 257 | { 258 | this.OnShutdown(this.boundThreadId, ex); 259 | return; 260 | } 261 | } 262 | else if (t.IsFaulted) 263 | { 264 | var ex = UnwrapIfRequired(t.Exception!); 265 | if (!this.InvokeUnhandledException(ex)) 266 | { 267 | this.OnShutdown(this.boundThreadId, ex); 268 | return; 269 | } 270 | } 271 | this.OnShutdown(this.boundThreadId, null); 272 | }); 273 | 274 | /// 275 | /// Execute message queue. 276 | /// 277 | /// Completion awaiting task 278 | public virtual void Run(Task task) 279 | { 280 | // Run only target thread. 281 | var currentThreadId = this.GetCurrentThreadId(); 282 | if (currentThreadId != this.boundThreadId) 283 | { 284 | throw new InvalidOperationException( 285 | $"Thread mismatch between created and running: Created={this.boundThreadId}, Running={currentThreadId}"); 286 | } 287 | 288 | this.HookTaskFinalizer(task); 289 | this.OnRun(this.boundThreadId); 290 | } 291 | 292 | /// 293 | /// Execute message queue. 294 | /// 295 | public virtual void Run() => 296 | this.Run(null!); 297 | 298 | /// 299 | /// Shutdown running context asynchronously. 300 | /// 301 | public void Shutdown() => 302 | this.OnShutdown(this.boundThreadId); 303 | } 304 | -------------------------------------------------------------------------------- /Lepracaun/UnhandledExceptionEventArgs.cs: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Lepracaun - Varies of .NET Synchronization Context. 4 | // Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | // 6 | // Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | // 8 | ///////////////////////////////////////////////////////////////////////////////////// 9 | 10 | using System; 11 | 12 | namespace Lepracaun; 13 | 14 | public sealed class UnhandledExceptionEventArgs : EventArgs 15 | { 16 | public readonly Exception Exception; 17 | public bool Handled; 18 | 19 | public UnhandledExceptionEventArgs(Exception ex) => 20 | this.Exception = ex; 21 | } 22 | -------------------------------------------------------------------------------- /Lepracaun/Win32MessagingSynchronizationContext.cs: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Lepracaun - Varies of .NET Synchronization Context. 4 | // Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | // 6 | // Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | // 8 | ///////////////////////////////////////////////////////////////////////////////////// 9 | 10 | using Lepracaun.Internal; 11 | using System; 12 | using System.Reflection; 13 | using System.Runtime.InteropServices; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | 17 | #if !NET35 && !NET40 18 | using System.Runtime.ExceptionServices; 19 | #endif 20 | 21 | namespace Lepracaun; 22 | 23 | /// 24 | /// Custom synchronization context implementation using Windows message queue (Win32) 25 | /// 26 | public sealed class Win32MessagingSynchronizationContext : 27 | ThreadBoundSynchronizationContext 28 | { 29 | /// 30 | /// Internal uses Windows message number (Win32). 31 | /// 32 | private static readonly int WM_SC; 33 | 34 | /// 35 | /// Type initializer. 36 | /// 37 | static Win32MessagingSynchronizationContext() => 38 | // Allocate Windows message number. 39 | // Using guid because type loaded into multiple AppDomain, type initializer called multiple. 40 | WM_SC = Win32NativeMethods.RegisterWindowMessageW( 41 | "Win32MessagingSynchronizationContext_" + Guid.NewGuid().ToString("N")); 42 | 43 | /// 44 | /// Constructor. 45 | /// 46 | public Win32MessagingSynchronizationContext() => 47 | this.SetTargetThreadId(Win32NativeMethods.GetCurrentThreadId()); 48 | 49 | protected override int GetCurrentThreadId() => 50 | Win32NativeMethods.GetCurrentThreadId(); 51 | 52 | /// 53 | /// Copy this context. 54 | /// 55 | /// Copied context. 56 | public override SynchronizationContext CreateCopy() => 57 | new Win32MessagingSynchronizationContext(); 58 | 59 | protected override void OnPost( 60 | int targetThreadId, SendOrPostCallback continuation, object? state) 61 | { 62 | // Get continuation and state cookie. 63 | // Because these values turn to unmanaged value (IntPtr), 64 | // so GC cannot track instances and maybe collects... 65 | var continuationCookie = GCHandle.ToIntPtr(GCHandle.Alloc(continuation)); 66 | var stateCookie = GCHandle.ToIntPtr(GCHandle.Alloc(state)); 67 | 68 | // Post continuation information into UI queue. 69 | Win32NativeMethods.PostThreadMessage( 70 | targetThreadId, WM_SC, continuationCookie, stateCookie); 71 | } 72 | 73 | protected override void OnRun( 74 | int targetThreadId) 75 | { 76 | // Run message loop (very legacy knowledge...) 77 | while (true) 78 | { 79 | // Get front of queue (or waiting). 80 | Win32NativeMethods.MSG msg; 81 | var result = Win32NativeMethods.GetMessage(out msg, IntPtr.Zero, 0, 0); 82 | 83 | // If message number is WM_QUIT (Cause PostQuitMessage API): 84 | if (result == 0) 85 | { 86 | if (msg.wParam != IntPtr.Zero) 87 | { 88 | // Retreive GCHandles from a cookie. 89 | var exceptionHandle = GCHandle.FromIntPtr(msg.wParam); 90 | 91 | // Retreive an exception. 92 | var exception = (Exception)exceptionHandle.Target!; 93 | 94 | // Release handle (Recollectable by GC) 95 | exceptionHandle.Free(); 96 | #if NET35 || NET40 97 | throw new TargetInvocationException(exception); 98 | #else 99 | var edi = ExceptionDispatchInfo.Capture(exception); 100 | edi.Throw(); 101 | #endif 102 | } 103 | 104 | // Exit. 105 | break; 106 | } 107 | 108 | // If cause error: 109 | if (result == -1) 110 | { 111 | // Throw. 112 | var hr = Marshal.GetHRForLastWin32Error(); 113 | Marshal.ThrowExceptionForHR(hr); 114 | } 115 | 116 | // If message is WM_SC: 117 | if (msg.msg == WM_SC) 118 | { 119 | // Retreive GCHandles from cookies. 120 | var continuationHandle = GCHandle.FromIntPtr(msg.wParam); 121 | var stateHandle = GCHandle.FromIntPtr(msg.lParam); 122 | 123 | // Retreive continuation and state. 124 | var continuation = (SendOrPostCallback)continuationHandle.Target!; 125 | var state = stateHandle.Target; 126 | 127 | // Release handle (Recollectable by GC) 128 | continuationHandle.Free(); 129 | stateHandle.Free(); 130 | 131 | // Invoke continuation. 132 | this.OnInvoke(continuation, state); 133 | 134 | // Consumed message. 135 | continue; 136 | } 137 | 138 | // Translate accelerator (require UI stability) 139 | Win32NativeMethods.TranslateMessage(ref msg); 140 | 141 | // Send to assigned window procedure. 142 | Win32NativeMethods.DispatchMessage(ref msg); 143 | } 144 | } 145 | 146 | protected override void OnShutdown( 147 | int targetThreadId, Exception? exception) 148 | { 149 | var exceptionCookie = exception != null ? 150 | GCHandle.ToIntPtr(GCHandle.Alloc(exception)) : IntPtr.Zero; 151 | 152 | Win32NativeMethods.PostThreadMessage( 153 | targetThreadId, Win32NativeMethods.WM_QUIT, exceptionCookie, IntPtr.Zero); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Lepracaun/WorkerThreadSynchronizationContext.cs: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Lepracaun - Varies of .NET Synchronization Context. 4 | // Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | // 6 | // Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | // 8 | ///////////////////////////////////////////////////////////////////////////////////// 9 | 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace Lepracaun; 14 | 15 | /// 16 | /// Custom synchronization context implementation using separated worker thread. 17 | /// 18 | public sealed class WorkerThreadSynchronizationContext : 19 | ManagedThreadSynchronizationContext 20 | { 21 | private readonly Thread thread; 22 | 23 | /// 24 | /// Constructor. 25 | /// 26 | public WorkerThreadSynchronizationContext() 27 | { 28 | this.thread = new(() => 29 | { 30 | SetSynchronizationContext(this); 31 | base.Run(null!); 32 | }); 33 | this.thread.IsBackground = true; 34 | base.SetTargetThreadId(this.thread.ManagedThreadId); 35 | } 36 | 37 | /// 38 | /// Copy this context. 39 | /// 40 | /// Copied context. 41 | public override SynchronizationContext CreateCopy() => 42 | new WorkerThreadSynchronizationContext(); 43 | 44 | /// 45 | /// Execute message queue. 46 | /// 47 | /// Completion awaiting task 48 | public override void Run(Task task) 49 | { 50 | this.HookTaskFinalizer(task); 51 | this.thread.Start(); 52 | } 53 | 54 | /// 55 | /// Execute message queue on background. 56 | /// 57 | public override void Run() => 58 | this.thread.Start(); 59 | } 60 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lepracaun 2 | 3 | ![Lepracaun](Images/Lepracaun.100.png) 4 | 5 | Lepracaun - Varies of .NET Synchronization Context. 6 | 7 | [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) 8 | 9 | ## NuGet 10 | 11 | | Package | NuGet | 12 | |:---------|:---------------------------------------------------------------------------------------------------------------------| 13 | | Lepracaun | [![NuGet Lepracaun](https://img.shields.io/nuget/v/Lepracaun.svg?style=flat)](https://www.nuget.org/packages/Lepracaun) | 14 | 15 | ---- 16 | 17 | ## What is this? 18 | 19 | Lepracaun is a library that collection of .NET Synchronization Context. It is a successor of [SynchContextSample library](https://github.com/kekyo/SynchContextSample). 20 | 21 | |Class|Detail| 22 | |:----|:----| 23 | |`SingleThreadedSynchronizationContext`|That constrains the execution context to a single thread using simple queue.| 24 | |`Win32MessagingSynchronizationContext`|That constrains the execution context to a single thread using the Win32 message pumps.| 25 | |`WorkerThreadSynchronizationContext`|Create a worker thread and running on it.| 26 | |`Application`|Pseudo (WPF/WinForms like) the Application class. Will use `SingleThreadedSynchronizationContext` defaulted.| 27 | 28 | ### Operating Environment 29 | 30 | The following platforms are supported by the package. 31 | 32 | * NET 8 to 5 33 | * NET Core 3.1 to 2.0 34 | * NET Standard 2.1 to 1.6 and 1.3 35 | * NET Framework 4.8.1 to 3.5 36 | 37 | ---- 38 | 39 | ## Basic usage 40 | 41 | ### Single threaded 42 | 43 | ```csharp 44 | // Allocate and assigned: 45 | using var sc = new SingleThreadedSynchronizationContext(); 46 | SynchronizationContext.SetSynchronizationContext(sc); 47 | 48 | // Use it directly (Post is asynchronously, so will delay execution.) 49 | var origin = Thread.CurrentThread.ManagedThreadId; 50 | sc.Post(_ => 51 | { 52 | var current = Thread.CurrentThread.ManagedThreadId; 53 | Console.WriteLine($"{origin} ==> {current}"); 54 | }, null); 55 | 56 | // Run it: 57 | sc.Run(); 58 | ``` 59 | 60 | ### Worker thread 61 | 62 | ```csharp 63 | // Allocate and assigned: 64 | using var sc = new WorkerThreadSynchronizationContext(); 65 | SynchronizationContext.SetSynchronizationContext(sc); 66 | 67 | // Use it directly (Post is asynchronously, so will delay execution.) 68 | var origin = Thread.CurrentThread.ManagedThreadId; 69 | sc.Post(_ => 70 | { 71 | var current = Thread.CurrentThread.ManagedThreadId; 72 | Console.WriteLine($"{origin} ==> {current}"); 73 | }, null); 74 | 75 | // Run it (Worker thread will run background.) 76 | sc.Run(); 77 | 78 | // (Wait until consume.) 79 | Thread.Sleep(1000); 80 | ``` 81 | 82 | ---- 83 | 84 | ## Realistic samples 85 | 86 | ### Main thread bound asynchronous operation 87 | 88 | ```csharp 89 | public static void Main(string[] args) 90 | { 91 | using var app = new Application(); 92 | 93 | //using var app = new Application( 94 | // new SingleThreadedSynchronizationContext()); 95 | 96 | app.Run(async () => 97 | { 98 | // Your asynchronous operations 99 | // into only main thread (Will not use any worker threads) 100 | 101 | using var rs = new FileStream(...); 102 | using var ws = new FileStream(...); 103 | 104 | var buffer = new byte[1024]; 105 | var read = await rs.ReadAsync(buffer, 0, buffer.Length); 106 | 107 | // (Rebound to main thread) 108 | await ws.WriteAsync(buffer, 0, read); 109 | 110 | // (Rebound to main thread) 111 | await ws.FlushAsync(); 112 | 113 | // (Rebound to main thread) 114 | 115 | // ... 116 | }); 117 | } 118 | ``` 119 | 120 | ### Bound temporary Win32 UI thread on any arbitrary thread context 121 | 122 | ```csharp 123 | public void MarshalInToUIThread() 124 | { 125 | using var app = new Application( 126 | new Win32MessagingSynchronizationContext()); 127 | 128 | // Run on worker thread. 129 | Task.Run(() => 130 | { 131 | // Some longer operations... 132 | 133 | // Manipulate Win32 UI from worker thread context. 134 | app.BeginInvoke(() => Win32NativeMethods.ShowWindow(this.window)); 135 | }); 136 | 137 | app.Run(); 138 | } 139 | ``` 140 | 141 | ---- 142 | 143 | ## License 144 | 145 | Apache-v2. 146 | 147 | ---- 148 | 149 | ## History 150 | 151 | * 1.1.0: 152 | * Fixed ignores any unhandled exceptions. (#1) 153 | * 1.0.0: 154 | * Improved queue consumer. 155 | * 0.4.0: 156 | * Supported .NET 7.0. 157 | * Fixed unmarking event field on `Application` class. 158 | * Fixed did not handle exception when runner bootstrap. 159 | * 0.3.0: 160 | * Added feature of worker thread bound. 161 | * 0.2.0: 162 | * Added CheckAccess method. 163 | * Fixed returns invalid identity. 164 | * 0.1.0: 165 | * Initial stable release. 166 | * Added pseudo `Application` class. 167 | * Fixed a problem that `SynchContext.Send()` completes even though the target continuation is not completed. 168 | * 0.0.2: 169 | * Initial release. 170 | -------------------------------------------------------------------------------- /build-nupkg.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem Lepracaun - Varies of .NET Synchronization Context. 4 | rem Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | rem 6 | rem Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | 8 | echo. 9 | echo "===========================================================" 10 | echo "Build Lepracaun" 11 | echo. 12 | 13 | rem git clean -xfd 14 | 15 | dotnet restore 16 | dotnet pack -p:Configuration=Release -o artifacts 17 | -------------------------------------------------------------------------------- /build-nupkg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Lepracaun - Varies of .NET Synchronization Context. 4 | # Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) 5 | # 6 | # Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 7 | 8 | echo "" 9 | echo "===========================================================" 10 | echo "Build Lepracaun" 11 | echo "" 12 | 13 | # git clean -xfd 14 | 15 | dotnet restore 16 | dotnet pack -p:Configuration=Release -o artifacts 17 | --------------------------------------------------------------------------------