├── .config └── dotnet-tools.json ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .paket └── Paket.Restore.targets ├── LICENSE ├── README.md ├── build.sh ├── paket.dependencies ├── paket.lock ├── protobuf-net-fsharp.sln ├── src └── ProtoBuf.FSharp │ ├── CodeGen.fs │ ├── MethodHelpers.fs │ ├── ProtoBuf.FSharp.fsproj │ ├── ProtobufUtils.fs │ ├── Surrogates.fs │ ├── ZeroValues.fs │ ├── app.config │ ├── paket.references │ └── paket.template └── test └── ProtoBuf.FSharp.Unit ├── CommonUtils.fs ├── Program.fs ├── ProtoBuf.FSharp.Unit.fsproj ├── TestRecordRoundtrip.fs ├── TestUnionRoundtrip.fs ├── app.config └── paket.references /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "paket": { 6 | "version": "7.2.1", 7 | "commands": [ 8 | "paket" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - dotnet-version: '3.1.x' 13 | dotnet-tfm: 'netcoreapp3.1' 14 | 15 | - dotnet-version: '5.0.x' 16 | dotnet-tfm: 'net5.0' 17 | 18 | - dotnet-version: '6.0.x' 19 | dotnet-tfm: 'net6.0' 20 | 21 | - dotnet-version: '7.0.x' 22 | dotnet-tfm: 'net7.0' 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Setup .NET ${{ matrix.dotnet-version }} 28 | uses: actions/setup-dotnet@v1 29 | with: 30 | dotnet-version: ${{ matrix.dotnet-version }} 31 | 32 | - name: Print .NET version 33 | run: dotnet --version 34 | 35 | - name: Substitute TargetFramework into test project 36 | working-directory: ./test/ProtoBuf.FSharp.Unit 37 | run: sed -i 's/.*<\/TargetFramework>/${{ matrix.dotnet-tfm }}<\/TargetFramework>/g' ProtoBuf.FSharp.Unit.fsproj 38 | 39 | - name: Paket 40 | run: dotnet tool restore 41 | 42 | - name: Build 43 | run: dotnet build 44 | 45 | - name: Run tests 46 | run: dotnet test 47 | -------------------------------------------------------------------------------- /.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/master/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 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Coverlet is a free, cross platform Code Coverage Tool 141 | coverage*[.json, .xml, .info] 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | 355 | ## 356 | ## Visual studio for Mac 357 | ## 358 | 359 | 360 | # globs 361 | Makefile.in 362 | *.userprefs 363 | *.usertasks 364 | config.make 365 | config.status 366 | aclocal.m4 367 | install-sh 368 | autom4te.cache/ 369 | *.tar.gz 370 | tarballs/ 371 | test-results/ 372 | 373 | # Mac bundle stuff 374 | *.dmg 375 | *.app 376 | 377 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 378 | # General 379 | .DS_Store 380 | .AppleDouble 381 | .LSOverride 382 | 383 | # Icon must end with two \r 384 | Icon 385 | 386 | 387 | # Thumbnails 388 | ._* 389 | 390 | # Files that might appear in the root of a volume 391 | .DocumentRevisions-V100 392 | .fseventsd 393 | .Spotlight-V100 394 | .TemporaryItems 395 | .Trashes 396 | .VolumeIcon.icns 397 | .com.apple.timemachine.donotpresent 398 | 399 | # Directories potentially created on remote AFP share 400 | .AppleDB 401 | .AppleDesktop 402 | Network Trash Folder 403 | Temporary Items 404 | .apdisk 405 | 406 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 407 | # Windows thumbnail cache files 408 | Thumbs.db 409 | ehthumbs.db 410 | ehthumbs_vista.db 411 | 412 | # Dump file 413 | *.stackdump 414 | 415 | # Folder config file 416 | [Dd]esktop.ini 417 | 418 | # Recycle Bin used on file shares 419 | $RECYCLE.BIN/ 420 | 421 | # Windows Installer files 422 | *.cab 423 | *.msi 424 | *.msix 425 | *.msm 426 | *.msp 427 | 428 | # Windows shortcuts 429 | *.lnk 430 | 431 | # JetBrains Rider 432 | .idea/ 433 | *.sln.iml 434 | 435 | ## 436 | ## Visual Studio Code 437 | ## 438 | .vscode/* 439 | !.vscode/settings.json 440 | !.vscode/tasks.json 441 | !.vscode/launch.json 442 | !.vscode/extensions.json 443 | -------------------------------------------------------------------------------- /.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | $(MSBuildVersion) 10 | 15.0.0 11 | false 12 | true 13 | 14 | true 15 | $(MSBuildThisFileDirectory) 16 | $(MSBuildThisFileDirectory)..\ 17 | $(PaketRootPath)paket-files\paket.restore.cached 18 | $(PaketRootPath)paket.lock 19 | classic 20 | proj 21 | assembly 22 | native 23 | /Library/Frameworks/Mono.framework/Commands/mono 24 | mono 25 | 26 | 27 | $(PaketRootPath)paket.bootstrapper.exe 28 | $(PaketToolsPath)paket.bootstrapper.exe 29 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ 30 | 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | True 42 | 43 | 44 | False 45 | 46 | $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | $(PaketRootPath)paket 56 | $(PaketToolsPath)paket 57 | 58 | 59 | 60 | 61 | 62 | $(PaketRootPath)paket.exe 63 | $(PaketToolsPath)paket.exe 64 | 65 | 66 | 67 | 68 | 69 | <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) 70 | <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) 71 | <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | <_PaketCommand>dotnet paket 83 | 84 | 85 | 86 | 87 | 88 | $(PaketToolsPath)paket 89 | $(PaketBootStrapperExeDir)paket 90 | 91 | 92 | paket 93 | 94 | 95 | 96 | 97 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 98 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" 99 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 100 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | true 122 | $(NoWarn);NU1603;NU1604;NU1605;NU1608 123 | false 124 | true 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 134 | 135 | 136 | 137 | 138 | 139 | 141 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) 142 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) 143 | 144 | 145 | 146 | 147 | %(PaketRestoreCachedKeyValue.Value) 148 | %(PaketRestoreCachedKeyValue.Value) 149 | 150 | 151 | 152 | 153 | true 154 | false 155 | true 156 | 157 | 158 | 162 | 163 | true 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached 183 | 184 | $(MSBuildProjectFullPath).paket.references 185 | 186 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 187 | 188 | $(MSBuildProjectDirectory)\paket.references 189 | 190 | false 191 | true 192 | true 193 | references-file-or-cache-not-found 194 | 195 | 196 | 197 | 198 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 199 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 200 | references-file 201 | false 202 | 203 | 204 | 205 | 206 | false 207 | 208 | 209 | 210 | 211 | true 212 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | false 224 | true 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) 236 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 237 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 238 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 239 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) 240 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[6]) 241 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[7]) 242 | 243 | 244 | %(PaketReferencesFileLinesInfo.PackageVersion) 245 | All 246 | runtime 247 | $(ExcludeAssets);contentFiles 248 | $(ExcludeAssets);build;buildMultitargeting;buildTransitive 249 | true 250 | true 251 | 252 | 253 | 254 | 255 | $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 265 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 266 | 267 | 268 | %(PaketCliToolFileLinesInfo.PackageVersion) 269 | 270 | 271 | 272 | 276 | 277 | 278 | 279 | 280 | 281 | false 282 | 283 | 284 | 285 | 286 | 287 | <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> 288 | 289 | 290 | 291 | 292 | 293 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 294 | true 295 | false 296 | true 297 | false 298 | true 299 | false 300 | true 301 | false 302 | true 303 | false 304 | true 305 | $(PaketIntermediateOutputPath)\$(Configuration) 306 | $(PaketIntermediateOutputPath) 307 | 308 | 309 | 310 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 370 | 371 | 420 | 421 | 466 | 467 | 511 | 512 | 555 | 556 | 557 | 558 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 mvkra 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Protobuf-Net FSharp Wrapper # 2 | 3 | This library is intended to provide a set of helper functions to allow F# types to be seriaised/deserialised using the protobuf-net library making it easier to use from a F# environment. 4 | 5 | [![NuGet Badge](http://img.shields.io/nuget/v/protobuf-net-fsharp.svg?style=flat)](https://www.nuget.org/packages/protobuf-net-fsharp) 6 | ![CI Badge](https://github.com/mvkara/protobuf-net-fsharp/actions/workflows/test.yml/badge.svg) 7 | 8 | ## Aims ## 9 | 10 | - Support common F# types (Options, Records, DU) 11 | - Leverage improvements/work done in protobuf-net library whilst adding F# specifc improvements on top (e.g. proto generation, gRpc, etc.) 12 | - Make it friendly to use from an F# environment (e.g. no implicit nulls during deserialisation where F# compiler doesn't expect them). 13 | 14 | ## Supported/Non supported features ## 15 | 16 | - Records 17 | - Discriminated Unions 18 | - Option fields on the above types 19 | - Empty arrays and strings are populated across the whole object graph during deserialisation to avoid unexpected null's in an F# context. 20 | 21 | Currently the non-supported features are mainly around the F# collections (Set, List, etc.). For the moment it is recommended to use arrays in your contract types to be serialised/deserialised. More work is required to support these. 22 | 23 | ## How to build/test ## 24 | 25 | Using Net Core 3.1 or above: 26 | 27 | ```bash 28 | dotnet tool restore 29 | dotnet build 30 | ``` 31 | 32 | To run tests: 33 | 34 | ```bash 35 | dotnet test 36 | ``` 37 | 38 | ## How to use ## 39 | 40 | All methods are in the ProtoBuf.FSharp.Serialiser module. They all require a previous RuntimeTypeModel 41 | to be created and/or retrieved in advance. Detailed examples are found in the unit test projects. 42 | 43 | A quick example of how to get the runtime model, and register both an existing record and DU type against it is as follows: 44 | 45 | ```fsharp 46 | open ProtoBuf.FSharp 47 | 48 | let model = 49 | RuntimeTypeModel.Create("Model") // OR RuntimeTypeModel.Default 50 | |> Serialiser.registerRecordIntoModel 51 | |> Serialiser.registerUnionIntoModel 52 | ``` 53 | 54 | An example below of how to use the model to serialise and deserialise an object assuming the code above has been run: 55 | 56 | ```fsharp 57 | let typeToTest = { RecordType.TestData = "TEST" } 58 | use ms = new MemoryStream() 59 | Serialiser.serialise model ms typeToTest 60 | ms.Seek(0L, SeekOrigin.Begin) |> ignore 61 | let rtData = Serialiser.deserialise<'t> model ms 62 | ``` 63 | 64 | ## Details 65 | 66 | ### Option Serialisation 67 | 68 | By default any usages of F# options in your registered records and/or unions are also registered into the Protobuf model. 69 | 70 | An example for the ```Option``` type the message as described in a proto file would be: 71 | 72 | ```protobuf 73 | message OptionalString { 74 | bool HasValue = 1; 75 | string Item = 2; 76 | } 77 | ``` 78 | 79 | The default behaviour for the name of the protobuf message type (in this case OptionalString) are: 80 | - All option types are prefixed with "Optional". 81 | - The suffix for the name is the parameter type's "Name" property. 82 | 83 | You can customise the suffix by registering the option type yourself but note this needs to be done before registering any types that use this type else the default behaviour of this library takes precedence. 84 | 85 | **Note**: As per the [Protobuf spec](https://developers.google.com/protocol-buffers/docs/proto3#default) default values can't be distinguished from missing values. The HasValue property distinguishes "None" cases from a (Some defaultValue) case. 86 | 87 | ### Default Value Serialisation 88 | 89 | Because default values are not serialised by Protobuf (empty strings, lists, etc) protobuf-net at time of writing when deserialising these objects often does not populate these fields leaving them with CLR null values. This breaks the F# assumption that fields on records and unions unless specified are not nullable and inhibits roundtrip serialisation. In other words serialising then deserialising an F# type does not preserve these values. This creates problems in an F# context especially with records and unions where constructors can't be overriden easily to prepopulate these types and the environment doesn't expect nulls to be definable on these types in normal use. 90 | 91 | This library adds a dynamic factory for registered types that populates the default values upon deserialsation for types. 92 | 93 | As an example the record 94 | ```fsharp 95 | type ExampleRecord = { TestOne: string; TestTwo: int array } 96 | let objectToSerialise = { TestOne = String.Empty; TestTwo = Array.empty } 97 | ``` 98 | will now deserialise as 99 | 100 | ```fsharp 101 | { TestOne = String.Empty; TestTwo = Array.empty } 102 | ``` 103 | 104 | vs the default protobuf-net behaviour at time of writing 105 | 106 | ```fsharp 107 | { TestOne = null; TestTwo = null } 108 | ``` 109 | 110 | ## Issues ## 111 | 112 | Any issues using this feel free to raise an issue on this repository. 113 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | dotnet tool restore 2 | dotnet build -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | storage:none 2 | source https://www.nuget.org/api/v2 3 | 4 | nuget FSharp.Core >= 4 redirects: force 5 | nuget protobuf-net >= 3.0 6 | nuget FsCheck >= 2.0 < 3.0 7 | nuget Expecto >= 9.0 < 10.0 8 | nuget Expecto.FsCheck >= 9.0 < 10.0 9 | nuget YoloDev.Expecto.TestSdk >= 0.13 < 0.14 10 | nuget Microsoft.NET.Test.Sdk 11 | -------------------------------------------------------------------------------- /paket.lock: -------------------------------------------------------------------------------- 1 | STORAGE: NONE 2 | NUGET 3 | remote: https://www.nuget.org/api/v2 4 | Expecto (9.0.4) 5 | FSharp.Core (>= 4.6) - restriction: || (>= net461) (>= netstandard2.0) 6 | Mono.Cecil (>= 0.11.3) - restriction: || (>= net461) (>= netstandard2.0) 7 | Expecto.FsCheck (9.0.4) 8 | Expecto (>= 9.0.4) - restriction: || (>= net461) (>= netstandard2.0) 9 | FsCheck (>= 2.14.3) - restriction: || (>= net461) (>= netstandard2.0) 10 | FsCheck (2.16.6) 11 | FSharp.Core (>= 4.0.0.1) - restriction: && (< net452) (>= netstandard1.0) (< netstandard1.6) 12 | FSharp.Core (>= 4.2.3) - restriction: || (>= net452) (>= netstandard1.6) 13 | FSharp.Core (7.0.400) - redirects: force 14 | Microsoft.CodeCoverage (17.7.2) - restriction: || (>= net462) (>= netcoreapp3.1) 15 | Microsoft.NET.Test.Sdk (17.7.2) 16 | Microsoft.CodeCoverage (>= 17.7.2) - restriction: || (>= net462) (>= netcoreapp3.1) 17 | Microsoft.TestPlatform.TestHost (>= 17.7.2) - restriction: >= netcoreapp3.1 18 | Microsoft.TestPlatform.ObjectModel (17.7.2) - restriction: >= netcoreapp3.1 19 | NuGet.Frameworks (>= 6.5) - restriction: || (>= net462) (>= netstandard2.0) 20 | System.Reflection.Metadata (>= 1.6) - restriction: || (>= net462) (>= netstandard2.0) 21 | Microsoft.TestPlatform.TestHost (17.7.2) - restriction: >= netcoreapp3.1 22 | Microsoft.TestPlatform.ObjectModel (>= 17.7.2) - restriction: >= netcoreapp3.1 23 | Newtonsoft.Json (>= 13.0.1) - restriction: >= netcoreapp3.1 24 | Mono.Cecil (0.11.5) - restriction: || (>= net461) (>= netstandard2.0) 25 | Newtonsoft.Json (13.0.3) - restriction: >= netcoreapp3.1 26 | NuGet.Frameworks (6.7) - restriction: >= netcoreapp3.1 27 | protobuf-net (3.2.26) 28 | protobuf-net.Core (>= 3.2.26) - restriction: || (>= net462) (>= netstandard2.0) 29 | System.Reflection.Emit (>= 4.7) - restriction: && (< net462) (>= netstandard2.0) (< netstandard2.1) 30 | System.Reflection.Emit.Lightweight (>= 4.7) - restriction: && (< net462) (>= netstandard2.0) (< netstandard2.1) 31 | protobuf-net.Core (3.2.26) - restriction: || (>= net462) (>= netstandard2.0) 32 | System.Collections.Immutable (>= 7.0) - restriction: || (>= net462) (>= netstandard2.0) 33 | System.Memory (>= 4.5.5) - restriction: || (>= net462) (&& (>= netstandard2.0) (< netstandard2.1)) 34 | System.Buffers (4.5.1) - restriction: || (&& (>= monoandroid) (< netstandard1.1) (>= netstandard2.0)) (&& (< monoandroid) (< netstandard1.1) (>= netstandard2.0) (< win8)) (&& (>= monotouch) (>= netstandard2.0)) (&& (< net45) (< netcoreapp2.0) (>= netstandard2.0) (< netstandard2.1)) (&& (>= net461) (>= netstandard2.0)) (>= net462) (&& (< netstandard1.1) (>= netstandard2.0) (>= win8)) (&& (>= netstandard2.0) (< netstandard2.1) (>= xamarintvos)) (&& (>= netstandard2.0) (< netstandard2.1) (>= xamarinwatchos)) (&& (< netstandard2.1) (>= xamarinios)) (&& (< netstandard2.1) (>= xamarinmac)) 35 | System.Collections.Immutable (7.0) - restriction: || (>= net462) (>= netstandard2.0) 36 | System.Memory (>= 4.5.5) - restriction: || (>= net462) (&& (< net6.0) (>= netstandard2.0)) 37 | System.Runtime.CompilerServices.Unsafe (>= 6.0) - restriction: || (>= net462) (&& (>= net6.0) (< net7.0)) (&& (< net6.0) (>= netstandard2.0)) 38 | System.Memory (4.5.5) - restriction: || (>= net462) (&& (< net6.0) (>= netcoreapp3.1)) (&& (>= netstandard2.0) (< netstandard2.1)) 39 | System.Buffers (>= 4.5.1) - restriction: || (&& (>= monoandroid) (< netstandard1.1)) (&& (< monoandroid) (< net45) (>= netstandard1.1) (< netstandard2.0) (< win8) (< wpa81)) (&& (< monoandroid) (< netstandard1.1) (>= portable-net45+win8+wpa81) (< win8)) (>= monotouch) (&& (>= net45) (< netstandard2.0)) (&& (< net45) (< netcoreapp2.0) (>= netstandard2.0)) (>= net461) (&& (< netstandard1.1) (>= win8)) (&& (< netstandard2.0) (< uap10.1) (>= wpa81)) (>= xamarinios) (>= xamarinmac) (>= xamarintvos) (>= xamarinwatchos) 40 | System.Numerics.Vectors (>= 4.4) - restriction: && (< net45) (< netcoreapp2.0) (>= netstandard2.0) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos) 41 | System.Numerics.Vectors (>= 4.5) - restriction: >= net461 42 | System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (&& (>= monoandroid) (< netstandard1.1)) (&& (< monoandroid) (< net45) (>= netstandard1.1) (< netstandard2.0) (< win8) (< wpa81)) (&& (< monoandroid) (>= netcoreapp2.0) (< netcoreapp2.1)) (&& (< monoandroid) (< netstandard1.1) (>= portable-net45+win8+wpa81) (< win8)) (>= monotouch) (&& (>= net45) (< netstandard2.0)) (&& (< net45) (< netcoreapp2.0) (>= netstandard2.0)) (>= net461) (&& (< netstandard1.1) (>= win8)) (&& (< netstandard2.0) (>= wpa81)) (>= uap10.1) (>= xamarinios) (>= xamarinmac) (>= xamarintvos) (>= xamarinwatchos) 43 | System.Numerics.Vectors (4.5) - restriction: || (&& (< net45) (< netcoreapp2.0) (>= netstandard2.0) (< netstandard2.1) (< xamarintvos) (< xamarinwatchos)) (&& (>= net461) (>= netstandard2.0)) (>= net462) 44 | System.Reflection.Emit (4.7) - restriction: && (< net462) (>= netstandard2.0) (< netstandard2.1) 45 | System.Reflection.Emit.ILGeneration (>= 4.7) - restriction: || (&& (< monoandroid) (< monotouch) (< net45) (>= netstandard1.1) (< netstandard2.0) (< win8) (< wpa81) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< net45) (< netcoreapp2.0) (>= netstandard2.0) (< netstandard2.1) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< netstandard1.1) (>= portable-net45+win8+wpa81) (< win8)) (&& (< netstandard1.1) (>= win8)) (&& (< netstandard2.0) (>= wpa81)) (>= uap10.1) 46 | System.Reflection.Emit.ILGeneration (4.7) - restriction: || (&& (< monoandroid) (< net45) (< netcoreapp2.0) (>= netstandard2.0) (< netstandard2.1) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< netstandard1.1) (>= netstandard2.0) (< win8)) (&& (< netstandard1.1) (>= netstandard2.0) (>= win8)) (&& (>= netstandard2.0) (>= uap10.1)) 47 | System.Reflection.Emit.Lightweight (4.7) - restriction: && (< net462) (>= netstandard2.0) (< netstandard2.1) 48 | System.Reflection.Emit.ILGeneration (>= 4.7) - restriction: || (&& (< monoandroid) (< monotouch) (< net45) (>= netstandard1.0) (< netstandard2.0) (< win8) (< wp8) (< wpa81) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< net45) (< netcoreapp2.0) (>= netstandard2.0) (< netstandard2.1) (< xamarintvos) (< xamarinwatchos)) (&& (< netstandard2.0) (>= wpa81)) (&& (>= portable-net45+win8+wp8+wpa81) (< portable-net45+wp8) (< win8)) (&& (< portable-net45+wp8) (>= win8)) (>= uap10.1) 49 | System.Reflection.Metadata (7.0.2) - restriction: >= netcoreapp3.1 50 | System.Collections.Immutable (>= 7.0) - restriction: || (>= net462) (>= netstandard2.0) 51 | System.Memory (>= 4.5.5) - restriction: || (>= net462) (&& (< net6.0) (>= netstandard2.0)) 52 | System.Runtime.CompilerServices.Unsafe (6.0) - restriction: || (&& (>= monoandroid) (< netstandard1.1) (>= netstandard2.0)) (&& (< monoandroid) (>= netcoreapp2.0) (< netcoreapp2.1) (< netstandard2.1)) (&& (< monoandroid) (< netstandard1.1) (>= netstandard2.0) (< win8)) (&& (>= monotouch) (>= netstandard2.0)) (&& (< net45) (< netcoreapp2.0) (>= netstandard2.0) (< netstandard2.1)) (&& (>= net461) (>= netstandard2.0)) (>= net462) (&& (>= net6.0) (< net7.0)) (&& (< net6.0) (>= netcoreapp3.1)) (&& (< netstandard1.1) (>= netstandard2.0) (>= win8)) (&& (>= netstandard2.0) (< netstandard2.1) (>= xamarintvos)) (&& (>= netstandard2.0) (< netstandard2.1) (>= xamarinwatchos)) (&& (>= netstandard2.0) (>= uap10.1)) (&& (< netstandard2.1) (>= xamarinios)) (&& (< netstandard2.1) (>= xamarinmac)) 53 | YoloDev.Expecto.TestSdk (0.13.3) 54 | Expecto (>= 9.0 < 10.0) - restriction: >= netcoreapp3.1 55 | FSharp.Core (>= 4.6.2) - restriction: >= netcoreapp3.1 56 | System.Collections.Immutable (>= 6.0) - restriction: >= netcoreapp3.1 57 | -------------------------------------------------------------------------------- /protobuf-net-fsharp.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.27004.2005 4 | MinimumVisualStudioVersion = 15.0.26124.0 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C96334A8-D895-4B8D-98C5-ED445002D10E}" 6 | EndProject 7 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "ProtoBuf.FSharp", "src\ProtoBuf.FSharp\ProtoBuf.FSharp.fsproj", "{2A86F63A-7757-4E5D-9767-A3E8B1288A09}" 8 | EndProject 9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{05FAEF74-6EB4-4B70-815B-623B6B47146C}" 10 | EndProject 11 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "ProtoBuf.FSharp.Unit", "test\ProtoBuf.FSharp.Unit\ProtoBuf.FSharp.Unit.fsproj", "{63D0255E-7F04-41BD-B116-616FDEF19426}" 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Debug|x64 = Debug|x64 17 | Debug|x86 = Debug|x86 18 | Release|Any CPU = Release|Any CPU 19 | Release|x64 = Release|x64 20 | Release|x86 = Release|x86 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {2A86F63A-7757-4E5D-9767-A3E8B1288A09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {2A86F63A-7757-4E5D-9767-A3E8B1288A09}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {2A86F63A-7757-4E5D-9767-A3E8B1288A09}.Debug|x64.ActiveCfg = Debug|Any CPU 26 | {2A86F63A-7757-4E5D-9767-A3E8B1288A09}.Debug|x64.Build.0 = Debug|Any CPU 27 | {2A86F63A-7757-4E5D-9767-A3E8B1288A09}.Debug|x86.ActiveCfg = Debug|Any CPU 28 | {2A86F63A-7757-4E5D-9767-A3E8B1288A09}.Debug|x86.Build.0 = Debug|Any CPU 29 | {2A86F63A-7757-4E5D-9767-A3E8B1288A09}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {2A86F63A-7757-4E5D-9767-A3E8B1288A09}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {2A86F63A-7757-4E5D-9767-A3E8B1288A09}.Release|x64.ActiveCfg = Release|Any CPU 32 | {2A86F63A-7757-4E5D-9767-A3E8B1288A09}.Release|x64.Build.0 = Release|Any CPU 33 | {2A86F63A-7757-4E5D-9767-A3E8B1288A09}.Release|x86.ActiveCfg = Release|Any CPU 34 | {2A86F63A-7757-4E5D-9767-A3E8B1288A09}.Release|x86.Build.0 = Release|Any CPU 35 | {63D0255E-7F04-41BD-B116-616FDEF19426}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {63D0255E-7F04-41BD-B116-616FDEF19426}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {63D0255E-7F04-41BD-B116-616FDEF19426}.Debug|x64.ActiveCfg = Debug|Any CPU 38 | {63D0255E-7F04-41BD-B116-616FDEF19426}.Debug|x64.Build.0 = Debug|Any CPU 39 | {63D0255E-7F04-41BD-B116-616FDEF19426}.Debug|x86.ActiveCfg = Debug|Any CPU 40 | {63D0255E-7F04-41BD-B116-616FDEF19426}.Debug|x86.Build.0 = Debug|Any CPU 41 | {63D0255E-7F04-41BD-B116-616FDEF19426}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {63D0255E-7F04-41BD-B116-616FDEF19426}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {63D0255E-7F04-41BD-B116-616FDEF19426}.Release|x64.ActiveCfg = Release|Any CPU 44 | {63D0255E-7F04-41BD-B116-616FDEF19426}.Release|x64.Build.0 = Release|Any CPU 45 | {63D0255E-7F04-41BD-B116-616FDEF19426}.Release|x86.ActiveCfg = Release|Any CPU 46 | {63D0255E-7F04-41BD-B116-616FDEF19426}.Release|x86.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(SolutionProperties) = preSolution 49 | HideSolutionNode = FALSE 50 | EndGlobalSection 51 | GlobalSection(NestedProjects) = preSolution 52 | {2A86F63A-7757-4E5D-9767-A3E8B1288A09} = {C96334A8-D895-4B8D-98C5-ED445002D10E} 53 | {63D0255E-7F04-41BD-B116-616FDEF19426} = {05FAEF74-6EB4-4B70-815B-623B6B47146C} 54 | EndGlobalSection 55 | GlobalSection(ExtensibilityGlobals) = postSolution 56 | SolutionGuid = {961FF62F-F871-431E-9363-C7DB21F52D94} 57 | EndGlobalSection 58 | EndGlobal 59 | -------------------------------------------------------------------------------- /src/ProtoBuf.FSharp/CodeGen.fs: -------------------------------------------------------------------------------- 1 | module internal ProtoBuf.FSharp.CodeGen 2 | 3 | open System 4 | open System.Collections.Concurrent 5 | open System.Collections.Generic 6 | open System.Threading 7 | open System.Reflection 8 | open System.Reflection.Emit 9 | open FSharp.Reflection 10 | open MethodHelpers 11 | 12 | 13 | type private TypeBuilder with 14 | member tb.DefineOpExplicit(src : Type, dst : Type) = 15 | let attr = MethodAttributes.Public ||| MethodAttributes.HideBySig ||| MethodAttributes.SpecialName ||| MethodAttributes.Static 16 | tb.DefineMethod("op_Explicit", attr, dst, [| src |]) 17 | 18 | member tb.SetProtoContractAttribute(skipConstructor : bool) = 19 | let t = typeof 20 | CustomAttributeBuilder( 21 | t.GetConstructor [||], [||] 22 | , [| t.GetProperty "ImplicitFields" ; t.GetProperty "SkipConstructor" |] 23 | , [| box ProtoBuf.ImplicitFields.AllFields ; box skipConstructor |] 24 | ) |> tb.SetCustomAttribute 25 | 26 | member tb.DefineFieldForProtobuf(fi : PropertyInfo) = 27 | tb.DefineField(fi.Name, fi.PropertyType, FieldAttributes.Public) // Do something with name and attributes? 28 | 29 | type private FieldBuilder with 30 | member fb.SetProtoMemberAttribute(tag : int) = 31 | let t = typeof 32 | CustomAttributeBuilder(t.GetConstructor [| typeof |], [| box tag |]) |> fb.SetCustomAttribute 33 | 34 | 35 | /// Hack to get default value of type 'tp' on top of evaluation stack: 36 | /// create local variable of type tp, initialize it with initobj opcode and read it. 37 | /// Used if 'tp' is value type (for reference types ldnull works ok) 38 | let private emitDefaultValueViaCell (gen : ILGenerator) (tp : Type) = 39 | let cell = gen.DeclareLocal(tp) 40 | gen.Emit(OpCodes.Ldloca_S, cell) 41 | gen.Emit(OpCodes.Initobj, tp) 42 | gen.Emit(OpCodes.Ldloc, cell) 43 | 44 | let private emitZeroValueOntoEvaluationStack (gen: ILGenerator) (getterType: MethodType) = 45 | match getterType with 46 | | MethodType.MethodInfo mi -> 47 | gen.EmitCall(OpCodes.Call, mi, null) 48 | | MethodType.PropertyInfo pi -> 49 | gen.EmitCall(OpCodes.Call, pi.GetMethod, null) 50 | | MethodType.FieldInfo fi -> 51 | gen.Emit(OpCodes.Ldsfld, fi) 52 | | MethodType.NewArray elementType -> 53 | gen.Emit(OpCodes.Ldc_I4_0) // Push length onto the stack. 54 | gen.Emit(OpCodes.Newarr, elementType) // Initialise array with length. 55 | 56 | /// Checks if value on top of evaluation stack (should be of type 'topType') is null 57 | /// and 'topType' has zero value defined. If so, replaces stack top with corresponding zero value. 58 | /// If 'topType' is generic parameter then the check is performed in runtime by calling 59 | /// ZeroValues.isApplicableTo and ZeroValues.getZeroValue<'t>. 60 | let private emitStackTopZeroCheck (gen : ILGenerator) (topType : Type) = 61 | if topType.IsGenericParameter then 62 | let skipZeroCheck = gen.DefineLabel() 63 | gen.Emit(OpCodes.Ldtoken, topType) // in runtime that loads whatever type topType is substituted with 64 | gen.Emit(OpCodes.Call, MethodHelpers.getMethodInfo <@ Type.GetTypeFromHandle @> [| |]) 65 | gen.Emit(OpCodes.Call, MethodHelpers.getMethodInfo <@ ZeroValues.isApplicableTo @> [| |]) 66 | gen.Emit(OpCodes.Brfalse, skipZeroCheck) 67 | gen.Emit(OpCodes.Dup) 68 | gen.Emit(OpCodes.Brtrue, skipZeroCheck) 69 | gen.Emit(OpCodes.Pop) 70 | gen.Emit(OpCodes.Call, MethodHelpers.getMethodInfo <@ ZeroValues.getZeroValue @> [| topType |]) 71 | gen.MarkLabel(skipZeroCheck) 72 | else 73 | ZeroValues.getZeroValueMethodInfoOpt topType |> Option.iter (fun getValue -> 74 | let skipZeroCheck = gen.DefineLabel() 75 | gen.Emit(OpCodes.Dup) 76 | gen.Emit(OpCodes.Brtrue, skipZeroCheck) 77 | gen.Emit(OpCodes.Pop) 78 | emitZeroValueOntoEvaluationStack gen getValue 79 | gen.MarkLabel(skipZeroCheck) 80 | ) 81 | 82 | let private emitFieldAssignments (gen: ILGenerator) (zeroValuesPerField: ZeroValues.FieldWithZeroValueSetMethod[]) = 83 | for zeroValueField in zeroValuesPerField do 84 | if zeroValueField.FieldInfo.IsStatic then 85 | emitZeroValueOntoEvaluationStack gen zeroValueField.ZeroValueMethod 86 | gen.Emit(OpCodes.Stsfld, zeroValueField.FieldInfo) // Assign to field 87 | else 88 | gen.Emit(OpCodes.Dup) 89 | emitZeroValueOntoEvaluationStack gen zeroValueField.ZeroValueMethod 90 | gen.Emit(OpCodes.Stfld, zeroValueField.FieldInfo) 91 | 92 | let private emitRecordDefault (gen: ILGenerator) (recordType: Type) = 93 | for pi in FSharpType.GetRecordFields(recordType, true) do 94 | let propertyType = pi.PropertyType 95 | 96 | match ZeroValues.getZeroValueMethodInfoOpt propertyType with 97 | | Some getValueMethodInfo -> 98 | emitZeroValueOntoEvaluationStack gen getValueMethodInfo 99 | | _ when propertyType.IsValueType -> 100 | emitDefaultValueViaCell gen propertyType 101 | | _ -> 102 | gen.Emit(OpCodes.Ldnull) 103 | 104 | let ctr = FSharpValue.PreComputeRecordConstructorInfo(recordType, true) 105 | gen.Emit(OpCodes.Newobj, ctr) 106 | 107 | 108 | let mutable private uniqueNameCounter = 0L 109 | 110 | 111 | /// Emits a factory to create the object making sure all values are default assigned as expected for F# consumption (e.g. no nulls where not possible to define for common cases) 112 | let private emitFactory (resultType : Type) (zeroValuesPerField: ZeroValues.FieldWithZeroValueSetMethod array) = 113 | let factoryMethod = DynamicMethod("factory_" + resultType.FullName, resultType, [| |], true) 114 | let gen = factoryMethod.GetILGenerator() 115 | 116 | match resultType.GetConstructor Array.empty with 117 | | null when FSharpType.IsRecord (resultType, true) -> // Is an F# record with a F# constructor. 118 | emitRecordDefault gen resultType 119 | | null -> // Is a type that isn't a record with no parameterless constructor. NOTE: This is significantly slower for deserialisation than alternative pathways. 120 | gen.Emit(OpCodes.Ldtoken, resultType) 121 | gen.Emit(OpCodes.Call, MethodHelpers.getMethodInfo <@ Type.GetTypeFromHandle @> [| |]) 122 | gen.EmitCall(OpCodes.Call, MethodHelpers.getMethodInfo <@ Runtime.Serialization.FormatterServices.GetUninitializedObject @> [||], null) 123 | emitFieldAssignments gen zeroValuesPerField 124 | | ctr -> // Has a parameterless constructor 125 | gen.Emit(OpCodes.Newobj, ctr) 126 | emitFieldAssignments gen zeroValuesPerField 127 | 128 | gen.Emit(OpCodes.Ret) 129 | factoryMethod :> MethodInfo 130 | 131 | 132 | let private getGenericArgs (t : Type) = 133 | if t.IsGenericTypeDefinition then 134 | t.GetGenericArguments() |> ValueSome 135 | else ValueNone 136 | 137 | let private defineGenericArgs (args : ValueOption) (tb : TypeBuilder) = 138 | args |> ValueOption.iter (fun args -> tb.DefineGenericParameters [| for arg in args -> arg.Name |] |> ignore) 139 | 140 | let private substituteGenericArgs args (t : Type) = 141 | match args with 142 | | ValueNone -> t 143 | | ValueSome args -> t.MakeGenericType args 144 | 145 | /// Adds to 'tb': 146 | /// * Fields representing 'targetFields' 147 | /// * Extract method to get 'targetType' value from surrogate 148 | /// * Constructor, that creates 'tb' from 'targetType' (if targetType is value type then it's passed by reference) 149 | let private emitSurrogateContent (tb : TypeBuilder) (targetType : Type) (targetFields : PropertyInfo[]) (targetGenerate : MethodBase) (isVirtual : bool) (baseDefaultConstructor : ConstructorInfo) = 150 | let fields = [| for fi in targetFields -> struct (fi, tb.DefineFieldForProtobuf(fi)) |] 151 | let constructor = 152 | let paramType = if targetType.IsValueType then targetType.MakeByRefType() else targetType 153 | tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, [| paramType |]) 154 | begin 155 | let gen = constructor.GetILGenerator() 156 | if not tb.IsValueType then // Call base class default constructor (if applicable) 157 | gen.Emit(OpCodes.Ldarg_0) 158 | gen.Emit(OpCodes.Call, match baseDefaultConstructor with | null -> typeof.GetConstructor [||] | ctr -> ctr) 159 | for (originField, surrogateField) in fields do 160 | gen.Emit(OpCodes.Ldarg_0) 161 | gen.Emit(OpCodes.Ldarg_1) 162 | gen.Emit(OpCodes.Call, originField.GetMethod) 163 | gen.Emit(OpCodes.Stfld, surrogateField) 164 | gen.Emit(OpCodes.Ret) 165 | end 166 | let extractMethod = 167 | let attr = if isVirtual then MethodAttributes.Public ||| MethodAttributes.Virtual else MethodAttributes.Public 168 | tb.DefineMethod("Extract", attr, targetType, [| |]) 169 | begin 170 | let gen = extractMethod.GetILGenerator() 171 | for (_, surrogateField) in fields do 172 | gen.Emit(OpCodes.Ldarg_0) 173 | gen.Emit(OpCodes.Ldfld, surrogateField) 174 | emitStackTopZeroCheck gen surrogateField.FieldType 175 | match targetGenerate with 176 | | :? ConstructorInfo as ctr -> gen.Emit(OpCodes.Newobj, ctr) 177 | | :? MethodInfo as method -> gen.Emit(OpCodes.Call, method) 178 | | smth -> failwithf "Expected constructor or static method, but got %A" smth 179 | gen.Emit(OpCodes.Ret) 180 | end 181 | struct (constructor, extractMethod) 182 | 183 | let private surrogatePrefix = "ProtoBuf.FSharp.Surrogates.Generated" 184 | 185 | /// Emits a record surrogate. Intended to be used to support value type records ONLY since Protobuf-net at time of writing does not support custom ValueTypes/Structs. 186 | let private emitRecordSurrogate (surrogateModule: ModuleBuilder) (recordType: Type) (defineSurrogateAsValueType: bool) = 187 | let genericArgs = getGenericArgs recordType 188 | let surrogateType = 189 | let name = sprintf "%s.%s" surrogatePrefix recordType.FullName 190 | let attr = TypeAttributes.Public ||| TypeAttributes.Sealed ||| TypeAttributes.Serializable 191 | if defineSurrogateAsValueType 192 | then surrogateModule.DefineType(name, attr, typeof) 193 | else surrogateModule.DefineType(name, attr) 194 | defineGenericArgs genericArgs surrogateType 195 | surrogateType.SetProtoContractAttribute(defineSurrogateAsValueType) 196 | let defaultConstructor = 197 | if surrogateType.IsValueType 198 | then ValueNone 199 | else surrogateType.DefineDefaultConstructor MethodAttributes.Public |> ValueSome 200 | 201 | let struct (constructor, extractMethod) = 202 | emitSurrogateContent surrogateType recordType 203 | (FSharpType.GetRecordFields(recordType, true)) 204 | (FSharpValue.PreComputeRecordConstructorInfo(recordType, true)) 205 | false null 206 | 207 | // Define op_Explicit methods that Protobuf calls to create recordType from surrogate. 208 | let conv = surrogateType.DefineOpExplicit(surrogateType, recordType) 209 | let gen = conv.GetILGenerator() 210 | gen.Emit((if surrogateType.IsValueType then OpCodes.Ldarga_S else OpCodes.Ldarg), 0) 211 | gen.Emit(OpCodes.Call, extractMethod) 212 | gen.Emit(OpCodes.Ret) 213 | 214 | // Define op_Explicit methods that Protobuf calls to create surrogate from recordType. 215 | let conv = surrogateType.DefineOpExplicit(recordType, surrogateType) 216 | let gen = conv.GetILGenerator() 217 | let argIsNotNull = gen.DefineLabel() 218 | if not recordType.IsValueType then // Check if argument is reference type and is not null 219 | gen.Emit(OpCodes.Ldarg_0) 220 | gen.Emit(OpCodes.Brtrue, argIsNotNull) 221 | match defaultConstructor with 222 | | ValueSome ctr -> gen.Emit(OpCodes.Newobj, ctr) 223 | | ValueNone -> emitDefaultValueViaCell gen surrogateType 224 | gen.Emit(OpCodes.Ret) 225 | gen.MarkLabel(argIsNotNull) 226 | gen.Emit((if recordType.IsValueType then OpCodes.Ldarga_S else OpCodes.Ldarg), 0) 227 | gen.Emit(OpCodes.Newobj, constructor) 228 | gen.Emit(OpCodes.Ret) 229 | 230 | surrogateType.CreateTypeInfo() 231 | 232 | /// Puts tag of union in arg 0 (of type 'unionType') on top of evaluation stack 233 | let private emitGetUnionTag (gen : ILGenerator) (unionType: Type) = 234 | gen.Emit((if unionType.IsValueType then OpCodes.Ldarga_S else OpCodes.Ldarg), 0) 235 | match FSharpValue.PreComputeUnionTagMemberInfo(unionType, true) with 236 | | :? PropertyInfo as tag -> gen.Emit(OpCodes.Call, tag.GetMethod) 237 | | :? MethodInfo as tag-> gen.Emit(OpCodes.Call, tag) 238 | | smth -> failwithf "Unexpected tag member: %A" smth 239 | 240 | /// Enumerate subtypes of union type that (maybe) represent union cases 241 | let relevantUnionSubtypes (unionType: Type) = seq { 242 | for tt in unionType.GetNestedTypes(BindingFlags.Public ||| BindingFlags.NonPublic) do 243 | let subtype = 244 | if unionType.IsGenericType && tt.IsGenericTypeDefinition 245 | then tt.MakeGenericType(unionType.GetGenericArguments()) 246 | else tt 247 | if subtype.IsSubclassOf unionType then 248 | yield subtype 249 | } 250 | 251 | /// Emit surrogate for UnionType in that style: 252 | /// stuct UnionSurrogate { 253 | /// public enum UnionTags { UnionTags_A, UnionTags_B, UnionTags_C, ... } 254 | /// public class CaseA { ... } 255 | /// public class CaseC { ... } 256 | /// ... 257 | /// public UnionTags Tag; 258 | /// public CaseA DataA; 259 | /// public CaseC DataC; 260 | /// ... 261 | /// } 262 | let private emitUnionSurrogateWithTag (surrogateModule: ModuleBuilder) (unionType: Type) = 263 | let genericArgs = getGenericArgs unionType 264 | let surrogateType = 265 | let name = sprintf "%s.%s" surrogatePrefix unionType.FullName 266 | let attr = TypeAttributes.Public ||| TypeAttributes.Sealed ||| TypeAttributes.Serializable 267 | surrogateModule.DefineType(name, attr, typeof) 268 | defineGenericArgs genericArgs surrogateType 269 | surrogateType.SetProtoContractAttribute(false) 270 | 271 | let tagEnum = 272 | let name = sprintf "UnionTags%i" (Interlocked.Increment &uniqueNameCounter) 273 | surrogateType.DefineNestedType(name, TypeAttributes.NestedPublic ||| TypeAttributes.Sealed, typeof, null) 274 | tagEnum.DefineField("value__", typeof, FieldAttributes.Private ||| FieldAttributes.SpecialName) |> ignore 275 | 276 | let surrogateTagField = surrogateType.DefineField("Tag", tagEnum, FieldAttributes.Public) 277 | surrogateTagField.SetProtoMemberAttribute(1) 278 | 279 | let cases = [| 280 | for caseInfo in FSharpType.GetUnionCases(unionType, true) -> 281 | let enumCase = 282 | let name = sprintf "%s_%s" tagEnum.Name caseInfo.Name 283 | tagEnum.DefineField(name, tagEnum, FieldAttributes.Public ||| FieldAttributes.Literal ||| FieldAttributes.Static) 284 | enumCase.SetConstant(caseInfo.Tag) 285 | let caseData = 286 | match caseInfo.GetFields() with 287 | | [||] -> ValueNone 288 | | caseFields -> 289 | let subtype = 290 | let attr = TypeAttributes.NestedPublic ||| TypeAttributes.Sealed ||| TypeAttributes.Serializable 291 | surrogateType.DefineNestedType("Case" + caseInfo.Name, attr) 292 | defineGenericArgs genericArgs subtype 293 | subtype.SetProtoContractAttribute(false) 294 | subtype.DefineDefaultConstructor MethodAttributes.Public |> ignore 295 | let struct (constructor, extractMethod) = 296 | emitSurrogateContent subtype unionType caseFields (FSharpValue.PreComputeUnionConstructorInfo(caseInfo, true)) false null 297 | subtype.CreateTypeInfo() |> ignore 298 | let caseDataField = surrogateType.DefineField("Data" + caseInfo.Name, subtype, FieldAttributes.Public) 299 | caseDataField.SetProtoMemberAttribute(2 + caseInfo.Tag) 300 | struct (caseDataField, constructor, extractMethod) |> ValueSome 301 | struct (caseInfo, caseData) 302 | |] 303 | 304 | // Define op_Explicit methods that Protobuf calls to create unionType from surrogate. 305 | let fromSurrogate = 306 | let conv = surrogateType.DefineOpExplicit(surrogateType, unionType) 307 | let gen = conv.GetILGenerator() 308 | 309 | let jumpTable = Array.init (Array.length cases) (ignore >> gen.DefineLabel) 310 | gen.Emit((if surrogateType.IsValueType then OpCodes.Ldarga_S else OpCodes.Ldarg), 0) 311 | gen.Emit(OpCodes.Ldfld, surrogateTagField) 312 | gen.Emit(OpCodes.Switch, jumpTable) 313 | 314 | for (caseInfo, caseStructure) in cases do 315 | gen.MarkLabel(jumpTable.[ caseInfo.Tag ]) 316 | match caseStructure with 317 | | ValueNone -> gen.Emit(OpCodes.Call, FSharpValue.PreComputeUnionConstructorInfo(caseInfo, true)) 318 | | ValueSome struct (caseDataField, _, extractMethod) -> 319 | gen.Emit(OpCodes.Ldarga_S, 0) 320 | gen.Emit(OpCodes.Ldfld, caseDataField) 321 | gen.Emit(OpCodes.Call, extractMethod) 322 | gen.Emit(OpCodes.Ret) 323 | conv 324 | 325 | // Define op_Explicit methods that Protobuf calls to create surrogate from unionType. 326 | let toSurrogate = 327 | let conv = surrogateType.DefineOpExplicit(unionType, surrogateType) 328 | let gen = conv.GetILGenerator() 329 | 330 | let resultCell = gen.DeclareLocal(surrogateType) 331 | gen.Emit(OpCodes.Ldloca_S, resultCell) 332 | gen.Emit(OpCodes.Initobj, surrogateType) 333 | 334 | let endLabel = gen.DefineLabel() 335 | if not unionType.IsValueType then // Check if argument is reference type and is null 336 | gen.Emit(OpCodes.Ldarg_0) 337 | gen.Emit(OpCodes.Brfalse, endLabel) 338 | 339 | let jumpTable = Array.init (Array.length cases) (ignore >> gen.DefineLabel) 340 | emitGetUnionTag gen unionType 341 | gen.Emit(OpCodes.Switch, jumpTable) // Dispatch on int union tag 342 | gen.Emit(OpCodes.Br, endLabel) 343 | 344 | for (caseInfo, caseStructure) in cases do 345 | gen.MarkLabel(jumpTable.[ caseInfo.Tag ]) 346 | caseStructure |> ValueOption.iter (fun struct (caseDataField, caseConstructor, _) -> 347 | // Create additional data for this union case and store it into corresponding field 348 | gen.Emit(OpCodes.Ldloca_S, resultCell) 349 | gen.Emit((if unionType.IsValueType then OpCodes.Ldarga_S else OpCodes.Ldarg), 0) 350 | gen.Emit(OpCodes.Newobj, caseConstructor) 351 | gen.Emit(OpCodes.Stfld, caseDataField) 352 | ) 353 | // Write tag 354 | gen.Emit(OpCodes.Ldloca_S, resultCell) 355 | gen.Emit(OpCodes.Ldc_I4, caseInfo.Tag) 356 | gen.Emit(OpCodes.Stfld, surrogateTagField) 357 | gen.Emit(OpCodes.Br, endLabel) 358 | 359 | gen.MarkLabel(endLabel) 360 | gen.Emit(OpCodes.Ldloc, resultCell) 361 | gen.Emit(OpCodes.Ret) 362 | conv 363 | 364 | // Create additional conversion operators for subtypes 365 | for subtype in relevantUnionSubtypes unionType do 366 | surrogateType 367 | .DefineOpExplicit(surrogateType, subtype) 368 | .GetILGenerator() 369 | .Emit(OpCodes.Jmp, fromSurrogate) 370 | 371 | surrogateType 372 | .DefineOpExplicit(subtype, surrogateType) 373 | .GetILGenerator() 374 | .Emit(OpCodes.Jmp, toSurrogate) 375 | 376 | tagEnum.CreateTypeInfo() |> ignore 377 | surrogateType.CreateTypeInfo () 378 | 379 | /// Emit surrogate for UnionType in that style: 380 | /// stuct UnionSurrogate { 381 | /// public abstract class Base { public abstract UnionType Extract(); } 382 | /// public class CaseA : Base { ... } 383 | /// public class CaseB : Base { ... } 384 | /// ... 385 | /// Base Tag; 386 | /// } 387 | let private emitUnionSurrogateWithSubtypes (surrogateModule: ModuleBuilder) (unionType: Type) = 388 | let genericArgs = getGenericArgs unionType 389 | let surrogateType = 390 | let name = sprintf "%s.%s" surrogatePrefix unionType.FullName 391 | let attr = TypeAttributes.Public ||| TypeAttributes.Sealed ||| TypeAttributes.Serializable 392 | surrogateModule.DefineType(name, attr, typeof) 393 | defineGenericArgs genericArgs surrogateType 394 | surrogateType.SetProtoContractAttribute(false) 395 | 396 | let caseBaseType = 397 | let attr = TypeAttributes.NestedPublic ||| TypeAttributes.Abstract ||| TypeAttributes.Serializable 398 | surrogateType.DefineNestedType("Base", attr) 399 | defineGenericArgs genericArgs caseBaseType 400 | caseBaseType.SetProtoContractAttribute(false) 401 | let baseDefaultConstructor = caseBaseType.DefineDefaultConstructor MethodAttributes.Public 402 | let extractBaseMethod = 403 | let attr = MethodAttributes.Public ||| MethodAttributes.Virtual ||| MethodAttributes.Abstract 404 | caseBaseType.DefineMethod("Extract", attr, unionType, [| |]) 405 | caseBaseType.CreateTypeInfo() |> ignore 406 | 407 | let surrogateTagField = surrogateType.DefineField("Tag", caseBaseType, FieldAttributes.Public) 408 | surrogateTagField.SetProtoMemberAttribute(1) 409 | 410 | let cases = [| 411 | for caseInfo in FSharpType.GetUnionCases(unionType, true) -> 412 | let subtype = 413 | let attr = TypeAttributes.NestedPublic ||| TypeAttributes.Sealed ||| TypeAttributes.Serializable 414 | surrogateType.DefineNestedType("Case" + caseInfo.Name, attr, caseBaseType) 415 | defineGenericArgs genericArgs subtype 416 | subtype.SetProtoContractAttribute(false) 417 | let struct (constructor, extractMethod) = 418 | emitSurrogateContent subtype unionType 419 | (caseInfo.GetFields()) 420 | (FSharpValue.PreComputeUnionConstructorInfo(caseInfo, true)) 421 | true baseDefaultConstructor 422 | subtype.DefineMethodOverride(extractMethod, if genericArgs.IsSome then TypeBuilder.GetMethod(caseBaseType, extractBaseMethod) else extractBaseMethod :> _) // Generic magic 423 | begin // DefineDefaultConstructor doesn't work here 424 | let ctr = subtype.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, [| |]) 425 | let gen = ctr.GetILGenerator() 426 | gen.Emit(OpCodes.Ldarg_0) 427 | gen.Emit(OpCodes.Call, baseDefaultConstructor) 428 | gen.Emit(OpCodes.Ret) 429 | end 430 | struct (caseInfo, subtype, constructor) 431 | |] 432 | 433 | // Define op_Explicit methods that Protobuf calls to create unionType from surrogate. 434 | let fromSurrogate = 435 | let conv = surrogateType.DefineOpExplicit(surrogateType, unionType) 436 | let gen = conv.GetILGenerator() 437 | gen.Emit((if surrogateType.IsValueType then OpCodes.Ldarga_S else OpCodes.Ldarg), 0) 438 | gen.Emit(OpCodes.Ldfld, surrogateTagField) 439 | gen.Emit(OpCodes.Callvirt, extractBaseMethod) 440 | gen.Emit(OpCodes.Ret) 441 | conv 442 | 443 | // Define op_Explicit methods that Protobuf calls to create surrogate from unionType. 444 | let toSurrogate = 445 | let conv = surrogateType.DefineOpExplicit(unionType, surrogateType) 446 | let gen = conv.GetILGenerator() 447 | 448 | let resultCell = gen.DeclareLocal(surrogateType) 449 | gen.Emit(OpCodes.Ldloca_S, resultCell) 450 | gen.Emit(OpCodes.Initobj, surrogateType) 451 | 452 | let endLabel = gen.DefineLabel() 453 | if not unionType.IsValueType then // Check if argument is reference type and is null 454 | gen.Emit(OpCodes.Ldarg_0) 455 | gen.Emit(OpCodes.Brfalse, endLabel) 456 | 457 | let jumpTable = Array.init (Array.length cases) (ignore >> gen.DefineLabel) 458 | emitGetUnionTag gen unionType 459 | gen.Emit(OpCodes.Switch, jumpTable) // Dispatch on int union tag 460 | gen.Emit(OpCodes.Br, endLabel) 461 | 462 | for (caseInfo, _, caseConstructor) in cases do 463 | gen.MarkLabel(jumpTable.[ caseInfo.Tag ]) 464 | gen.Emit(OpCodes.Ldloca_S, resultCell) 465 | gen.Emit((if unionType.IsValueType then OpCodes.Ldarga_S else OpCodes.Ldarg), 0) 466 | gen.Emit(OpCodes.Newobj, caseConstructor) 467 | gen.Emit(OpCodes.Stfld, surrogateTagField) 468 | gen.Emit(OpCodes.Br, endLabel) 469 | 470 | gen.MarkLabel(endLabel) 471 | gen.Emit(OpCodes.Ldloc, resultCell) 472 | gen.Emit(OpCodes.Ret) 473 | conv 474 | 475 | // Create additional conversion operators for subtypes 476 | for subtype in relevantUnionSubtypes unionType do 477 | surrogateType 478 | .DefineOpExplicit(surrogateType, subtype) 479 | .GetILGenerator() 480 | .Emit(OpCodes.Jmp, fromSurrogate) 481 | 482 | surrogateType 483 | .DefineOpExplicit(subtype, surrogateType) 484 | .GetILGenerator() 485 | .Emit(OpCodes.Jmp, toSurrogate) 486 | 487 | // This method is called by Serialiser.registerSurrogate with reflection 488 | // It calls AddSubType for all subtypes of Base (attribute doesn't appear to work with emitted classes for some reason) 489 | begin 490 | let method = surrogateType.DefineMethod("RegisterIntoModel", MethodAttributes.Public ||| MethodAttributes.Static, null, [| typeof |]) 491 | let gen = method.GetILGenerator() 492 | let metaTypeCell = gen.DeclareLocal(typeof) 493 | gen.Emit(OpCodes.Ldarg_0) 494 | gen.Emit(OpCodes.Ldtoken, substituteGenericArgs genericArgs caseBaseType) 495 | gen.Emit(OpCodes.Ldc_I4_1) 496 | gen.Emit(OpCodes.Call, typeof.GetMethod("Add")) 497 | gen.Emit(OpCodes.Stloc, metaTypeCell) 498 | for (caseInfo, subclass, _) in cases do 499 | gen.Emit(OpCodes.Ldloc, metaTypeCell) 500 | gen.Emit(OpCodes.Ldc_I4, 1000 + caseInfo.Tag) 501 | gen.Emit(OpCodes.Ldtoken, substituteGenericArgs genericArgs subclass) 502 | gen.Emit(OpCodes.Call, typeof.GetMethod("AddSubType", [| typeof ; typeof |])) 503 | gen.Emit(OpCodes.Pop) 504 | gen.Emit(OpCodes.Ret) 505 | end 506 | 507 | let surrogate = surrogateType.CreateTypeInfo () 508 | for (_, sub, _) in cases do 509 | sub.CreateTypeInfo() |> ignore 510 | surrogate 511 | 512 | 513 | let private surrogateAssembly = AssemblyBuilder.DefineDynamicAssembly(AssemblyName("SurrogateAssembly"), AssemblyBuilderAccess.Run) 514 | let private surrogateModule = surrogateAssembly.DefineDynamicModule "SurrogateModule" 515 | let private surrogateCache = 516 | ConcurrentDictionary> (seq { 517 | KeyValuePair(typedefof>, lazy typedefof>) 518 | }) 519 | 520 | let private makeSurrogate (typeToAdd : Type) = 521 | match typeToAdd with 522 | | t when FSharpType.IsUnion(t, true) -> 523 | if t.IsValueType then 524 | lazy (emitUnionSurrogateWithTag surrogateModule typeToAdd :> Type) 525 | else 526 | lazy (emitUnionSurrogateWithSubtypes surrogateModule typeToAdd :> Type) 527 | | t when FSharpType.IsRecord(t, true) -> 528 | lazy (emitRecordSurrogate surrogateModule typeToAdd true :> Type) 529 | | t -> 530 | failwithf "No surrogate construction method for type %A" t 531 | 532 | let getSurrogate (typeToAdd : Type) = 533 | if typeToAdd.IsGenericType then 534 | let surrogateDef = surrogateCache.GetOrAdd(typeToAdd.GetGenericTypeDefinition(), makeSurrogate).Value 535 | surrogateDef.MakeGenericType(typeToAdd.GetGenericArguments()) 536 | else 537 | surrogateCache.GetOrAdd(typeToAdd, makeSurrogate).Value 538 | 539 | 540 | let private factoryCache = ConcurrentDictionary>() 541 | 542 | let getFactory (typeToAdd : Type) zeroValuesForFields = 543 | factoryCache.GetOrAdd(typeToAdd, fun _ -> lazy emitFactory typeToAdd zeroValuesForFields).Value 544 | -------------------------------------------------------------------------------- /src/ProtoBuf.FSharp/MethodHelpers.fs: -------------------------------------------------------------------------------- 1 | module internal ProtoBuf.FSharp.MethodHelpers 2 | 3 | open System 4 | open Microsoft.FSharp.Quotations.Patterns 5 | open System.Reflection 6 | 7 | /// Allows you to get the nameof a method in older F# versions 8 | let nameOfQuotation methodQuotation = 9 | match methodQuotation with 10 | | Lambda(_, Call(_, mi, _)) 11 | | Lambda(_, Lambda(_, Call(_, mi, _))) -> mi.DeclaringType, mi.Name 12 | | FieldGet(_, fi) -> (fi.DeclaringType, fi.Name) 13 | | x -> failwithf "Not supported %A" x 14 | 15 | let bindingFlagsToUse = BindingFlags.Public ||| BindingFlags.NonPublic ||| BindingFlags.Static 16 | 17 | let getMethodInfo quotation (typeParameters: Type array) = 18 | let (declaringType, nameOfMethod) = nameOfQuotation quotation 19 | match typeParameters.Length with 20 | | 0 -> declaringType.GetMethod(nameOfMethod, bindingFlagsToUse) 21 | | _ -> declaringType.GetMethod(nameOfMethod, bindingFlagsToUse).MakeGenericMethod(typeParameters) 22 | 23 | let getPropertyInfo quotation = 24 | let (declaringType, nameOfMethod) = nameOfQuotation quotation 25 | declaringType.GetProperty(nameOfMethod, bindingFlagsToUse) 26 | 27 | [] 28 | type MethodType = 29 | | MethodInfo of MethodInfo 30 | | PropertyInfo of PropertyInfo 31 | | FieldInfo of FieldInfo 32 | | NewArray of elementType : Type 33 | 34 | let getFetchFunc methodQuotation (typeParameters: Type array) = 35 | match methodQuotation with 36 | | Call(_, mi, _) when Array.isEmpty typeParameters -> MethodType.MethodInfo mi 37 | | Call(_, mi, _) -> mi.GetGenericMethodDefinition().MakeGenericMethod(typeParameters) |> MethodType.MethodInfo 38 | | Lambda(_, Call(_, mi, _)) 39 | | Lambda(_, Lambda(_, Call(_, mi, _))) -> MethodType.MethodInfo (mi.MakeGenericMethod(typeParameters)) 40 | | FieldGet(_, fi) -> MethodType.FieldInfo fi 41 | | x -> failwithf "Not supported %A" x -------------------------------------------------------------------------------- /src/ProtoBuf.FSharp/ProtoBuf.FSharp.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | protobuf-net-fsharp 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ProtoBuf.FSharp/ProtobufUtils.fs: -------------------------------------------------------------------------------- 1 | namespace ProtoBuf.FSharp 2 | 3 | open ProtoBuf 4 | open ProtoBuf.Meta 5 | open FSharp.Reflection 6 | open System 7 | open System.Reflection 8 | open System.IO 9 | 10 | module Serialiser = 11 | let private registerSurrogate (tp : Type) (model : RuntimeTypeModel) = 12 | let surrogateType = CodeGen.getSurrogate tp 13 | match surrogateType.GetMethod("RegisterIntoModel") with 14 | | null -> () 15 | | method -> method.Invoke(null, [| box model |]) |> ignore 16 | model.Add(tp, false).SetSurrogate surrogateType 17 | surrogateType 18 | 19 | /// The magic number where if a union type has more than the above cases it simply is a tagged instance of the parent type. 20 | /// Otherwise for this number and below even non-empty unions get their own inner class prefixed with "_". 21 | let [] private CasesCountWhereNoFieldCasesGenerateType = 3 22 | 23 | /// Allows users to register option types in advance. You can specify a custom suffix name for the Protobuf wrapper type generated. 24 | /// This only needs to be called directly if your type either is not already a field in another type previously registered (e.g. a record or union) 25 | /// and/or your not happy with the default type name in case of naming clashes. 26 | /// By default if None is provided for the customTypeSuffix parameter for example with Option the protobuf message will be an "OptionalString". 27 | /// If the model is already registered (explictly or implicitly via another registration) AND/OR the type passed in is not an option type this will no-op. 28 | let registerOptionTypesIntoModel (optionType: Type) customTypeSuffix (model: RuntimeTypeModel) = 29 | if optionType.IsGenericType && optionType.GetGenericTypeDefinition() = typedefof> then 30 | let definedTypes = seq { 31 | for m in model.GetTypes() do 32 | let m = m :?> MetaType 33 | yield m.Type 34 | } 35 | if definedTypes |> Seq.contains optionType |> not 36 | then 37 | registerSurrogate optionType model |> ignore 38 | 39 | let private processFieldsAndCreateFieldSetters (typeToAdd: Type) (model : RuntimeTypeModel) = 40 | let metaType = model.Add(typeToAdd, false) 41 | metaType.UseConstructor <- false 42 | 43 | let fields = typeToAdd.GetFields(BindingFlags.Public ||| BindingFlags.NonPublic ||| BindingFlags.Instance ||| BindingFlags.GetField) 44 | for (index, fieldInfo) in Seq.indexed fields do 45 | let fieldModel = metaType.AddField(1 + index, fieldInfo.Name) 46 | fieldModel.BackingMember <- fieldInfo 47 | fieldModel.OverwriteList <- true 48 | fieldModel.Name <- fieldInfo.Name.TrimStart('_').TrimEnd('@') // Still not perfect --- F# allows pretty wild names (some cause protobuf to fail) 49 | 50 | let zeroValuesForFields = ZeroValues.calculateApplicableFields fields 51 | if Array.length zeroValuesForFields > 0 then 52 | metaType.SetFactory(CodeGen.getFactory typeToAdd zeroValuesForFields) |> ignore 53 | 54 | metaType 55 | 56 | let private registerUnionDirectly (unionType: Type) (model: RuntimeTypeModel) = 57 | let unionCaseData = FSharpType.GetUnionCases(unionType, true) 58 | 59 | // Register the supertype in all cases 60 | let mt = processFieldsAndCreateFieldSetters unionType model 61 | 62 | // If there are no fields in any properties then we can assume the F# compiler has compiled 63 | // the class in a non-flat fashion. Structs are still compiled in a flat way (F# 4.1+ struct DU's). 64 | // Note: Protobuf doesn't quite support custom factories of structs failing at the verification so we are not supporting this. 65 | let isReferenceMulticaseDuWithPayload = not (unionType.IsValueType || unionCaseData |> Seq.collect (fun x -> x.GetFields()) |> Seq.isEmpty) 66 | 67 | if isReferenceMulticaseDuWithPayload 68 | then 69 | if unionCaseData.Length > 1 // Otherwise all fields would already be populated in the union root type before this IF statement. 70 | then 71 | for ucd in unionCaseData do 72 | let candidateTypes = unionType.GetNestedTypes(BindingFlags.Public ||| BindingFlags.NonPublic) 73 | 74 | let typeToAddOpt = 75 | candidateTypes 76 | |> Seq.tryFind (fun x -> x.Name = ucd.Name || x.Name = "_" + ucd.Name) // For under 3 cases classes with no fields are private with a "_" prefix. 77 | |> Option.map 78 | (fun typeToAdd -> 79 | // Handle generic typed unions 80 | if unionType.IsGenericType && typeToAdd.IsGenericTypeDefinition 81 | then typeToAdd.MakeGenericType(unionType.GetGenericArguments()) 82 | else typeToAdd) 83 | 84 | let typeToAddOpt = 85 | match typeToAddOpt with 86 | | Some(t) -> Some t 87 | | None -> 88 | if ucd.GetFields().Length = 0 && unionCaseData.Length > CasesCountWhereNoFieldCasesGenerateType 89 | then None // In this case the "Tag" field is used by the F# compiler rather than an instance test. 90 | else 91 | failwithf 92 | "Couldn't find expected type for union case [UnionType: %A, InnerCaseName: %s, UnionCaseInfo: %A, CandidateTypes: %A]" 93 | unionType.FullName ucd.Name ucd (candidateTypes |> Seq.map (fun x -> x.FullName)) 94 | 95 | // The union may be a supertype union with no values hence no subtype. Should use the supertype as appropriate and skip this case. 96 | match typeToAddOpt with 97 | | Some(typeToAdd) -> 98 | let caseTypeModel = processFieldsAndCreateFieldSetters typeToAdd model 99 | caseTypeModel.Name <- ucd.Name 100 | let tag = 1000 + ucd.Tag 101 | mt.AddSubType(tag, typeToAdd) |> ignore 102 | | None -> () 103 | 104 | let private internalRegister useSurrogateForReferenceUnions (runtimeType: Type) (model: RuntimeTypeModel) = 105 | match runtimeType with 106 | | recordType when FSharpType.IsRecord(recordType, true) -> 107 | let fields = FSharpType.GetRecordFields(recordType, true) 108 | if recordType.IsValueType && fields |> Array.exists (fun pi -> ZeroValues.isApplicableTo pi.PropertyType) then 109 | registerSurrogate recordType model |> ignore 110 | else 111 | processFieldsAndCreateFieldSetters recordType model |> ignore 112 | 113 | for field in fields do 114 | registerOptionTypesIntoModel field.PropertyType None model 115 | 116 | | unionType when FSharpType.IsUnion(unionType, true) -> 117 | if unionType.IsGenericType && unionType.GetGenericTypeDefinition() = typedefof> then 118 | registerOptionTypesIntoModel unionType None model 119 | elif unionType.IsValueType || useSurrogateForReferenceUnions then 120 | let surrogateType = registerSurrogate unionType model 121 | for subtype in CodeGen.relevantUnionSubtypes unionType do 122 | model.Add(subtype, false).SetSurrogate(surrogateType) 123 | else 124 | registerUnionDirectly unionType model 125 | 126 | for caseInfo in FSharpType.GetUnionCases(unionType, true) do 127 | for field in caseInfo.GetFields() do 128 | registerOptionTypesIntoModel field.PropertyType None model 129 | 130 | | _ -> 131 | model.Add(runtimeType, true) |> ignore 132 | 133 | 134 | let registerRuntimeTypeIntoModel (runtimeType: Type) (model: RuntimeTypeModel) = 135 | internalRegister false runtimeType model 136 | model 137 | 138 | let registerTypeIntoModel<'t> (model: RuntimeTypeModel) = 139 | registerRuntimeTypeIntoModel typeof<'t> model 140 | 141 | 142 | let registerUnionRuntimeTypeIntoModel (unionType: Type) (model: RuntimeTypeModel) = 143 | if FSharpType.IsUnion(unionType, true) then 144 | registerRuntimeTypeIntoModel unionType model 145 | else 146 | failwithf "registerUnionRuntimeTypeIntoModel: %A is not a union" unionType 147 | 148 | let registerUnionIntoModel<'tunion> model = 149 | registerUnionRuntimeTypeIntoModel typeof<'tunion> model 150 | 151 | 152 | let registerRecordRuntimeTypeIntoModel (recordType: Type) (model: RuntimeTypeModel) = 153 | if FSharpType.IsRecord(recordType, true) then 154 | registerRuntimeTypeIntoModel recordType model 155 | else 156 | failwithf "registerRecordRuntimeTypeIntoModel: %A is not a record" recordType 157 | 158 | let registerRecordIntoModel<'t> (model: RuntimeTypeModel) = 159 | registerRecordRuntimeTypeIntoModel typeof<'t> model 160 | 161 | 162 | let serialise (model: RuntimeTypeModel) (stream: Stream) (o: 't) = model.Serialize(stream, o) 163 | 164 | let deserialise<'t> (model: RuntimeTypeModel) (stream: Stream) = model.Deserialize(stream, null, typeof<'t>) :?> 't 165 | 166 | let defaultModel = RuntimeTypeModel.Default -------------------------------------------------------------------------------- /src/ProtoBuf.FSharp/Surrogates.fs: -------------------------------------------------------------------------------- 1 | namespace ProtoBuf.FSharp.Surrogates 2 | 3 | open ProtoBuf 4 | open ProtoBuf.Meta 5 | open ProtoBuf.FSharp.ZeroValues 6 | 7 | [] 8 | type Optional<'t> = 9 | { [] HasValue: bool 10 | [] Item: 't } 11 | 12 | static member op_Implicit (o: 't option) = 13 | match o with 14 | | Some(o) -> { Item = o; HasValue = true } 15 | | None -> { HasValue = false; Item = Unchecked.defaultof<_> } 16 | 17 | static member op_Implicit (w: Optional<'t>) : 't option = 18 | match w.HasValue with 19 | | false -> None 20 | | true when isApplicableTo typeof<'t> -> w.Item |> fixZeroValue |> Some 21 | | true -> Some w.Item 22 | 23 | static member RegisterIntoModel (model : RuntimeTypeModel) = 24 | // For some reason on .net 7 that throws: 25 | // System.TypeLoadException: Could not load type 'System.Runtime.CompilerServices.IsReadOnlyAttribute' from assembly 'System.Collections.Immutable, Version=7.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' 26 | //let surrogateModelType = model.Add(typeof>, true) 27 | 28 | let surrogateModelType = model.Add(typeof>, false) 29 | surrogateModelType.Add("HasValue", "Item") |> ignore 30 | surrogateModelType.Name <- "Optional" + typeof<'t>.Name 31 | -------------------------------------------------------------------------------- /src/ProtoBuf.FSharp/ZeroValues.fs: -------------------------------------------------------------------------------- 1 | namespace ProtoBuf.FSharp 2 | 3 | open System 4 | open System.Collections.Concurrent 5 | open System.Reflection 6 | 7 | module ZeroValues = 8 | 9 | type internal FieldWithZeroValueSetMethod = { FieldInfo: FieldInfo; ZeroValueMethod: MethodHelpers.MethodType } 10 | 11 | let private zeroValueFieldSetters = ConcurrentDictionary() 12 | 13 | let internal getZeroValueMethodInfoOpt (fieldType: Type) = 14 | /// Creates the zero value for supported types that we know of. 15 | let createZeroValueMethodInfoSetter() = 16 | if fieldType = typeof then 17 | MethodHelpers.getFetchFunc <@ String.Empty @> [| |] |> Some 18 | elif fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() = typedefof<_ list> then 19 | MethodHelpers.getFetchFunc <@ List.empty @> fieldType.GenericTypeArguments |> Some 20 | elif fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() = typedefof> then 21 | MethodHelpers.getFetchFunc <@ Set.empty @> fieldType.GenericTypeArguments |> Some 22 | elif fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() = typedefof> then 23 | MethodHelpers.getFetchFunc <@ Map.empty @> fieldType.GenericTypeArguments |> Some 24 | elif fieldType.IsArray then 25 | fieldType.GetElementType() |> MethodHelpers.MethodType.NewArray |> Some 26 | else None 27 | 28 | match zeroValueFieldSetters.TryGetValue(fieldType) with 29 | | (true, zeroValue) -> Some zeroValue 30 | | (false, _) -> 31 | match createZeroValueMethodInfoSetter() with 32 | | Some(zeroValue) -> 33 | zeroValueFieldSetters.[fieldType] <- zeroValue 34 | Some zeroValue 35 | | None -> None 36 | 37 | let internal calculateApplicableFields (fields: FieldInfo[]) = [| 38 | for fi in fields do 39 | match getZeroValueMethodInfoOpt fi.FieldType with 40 | | None -> () 41 | | Some getZeroValueMethod -> { FieldInfo = fi; ZeroValueMethod = getZeroValueMethod } 42 | |] 43 | 44 | let isApplicableTo (t : Type) = 45 | getZeroValueMethodInfoOpt t |> Option.isSome 46 | 47 | let getZeroValue<'t> () : 't = 48 | match getZeroValueMethodInfoOpt typeof<'t> with 49 | | Some (MethodHelpers.MethodType.NewArray elType) -> 50 | System.Array.CreateInstance(elType, 0) |> unbox 51 | | Some (MethodHelpers.MethodType.MethodInfo mi) -> 52 | mi.Invoke(null, null) |> unbox 53 | | Some (MethodHelpers.MethodType.FieldInfo fi) -> 54 | fi.GetValue(null) |> unbox 55 | | Some (MethodHelpers.MethodType.PropertyInfo pi) -> 56 | pi.GetValue(null) |> unbox 57 | | None -> 58 | sprintf "No zero value for %A" typeof<'t> |> NotImplementedException |> raise 59 | 60 | let internal fixZeroValue<'t> (x : 't) = 61 | match box x with 62 | | null -> getZeroValue<'t> () 63 | | _ -> x -------------------------------------------------------------------------------- /src/ProtoBuf.FSharp/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | True 6 | 7 | 8 | 9 | 10 | True 11 | 12 | 13 | 14 | 15 | True 16 | 17 | 18 | 19 | 20 | True 21 | 22 | 23 | 24 | 25 | True 26 | 27 | 28 | 29 | 30 | True 31 | 32 | 33 | 34 | 35 | True 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/ProtoBuf.FSharp/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | protobuf-net -------------------------------------------------------------------------------- /src/ProtoBuf.FSharp/paket.template: -------------------------------------------------------------------------------- 1 | type project 2 | id protobuf-net-fsharp 3 | version 0.2.1 4 | authors mvkra 5 | description Allows F# types to work with protobuf-net 6 | summary Allows F# types to work with protobuf-net 7 | projectUrl https://github.com/mvkara/protobuf-net-fsharp 8 | tags protobuf protobuf-net fsharp f# 9 | licenseUrl https://spdx.org/licenses/MIT.html 10 | include-pdbs true 11 | -------------------------------------------------------------------------------- /test/ProtoBuf.FSharp.Unit/CommonUtils.fs: -------------------------------------------------------------------------------- 1 | namespace ProtoBuf.FSharp.Unit 2 | 3 | open System 4 | 5 | type TestNameAttribute(name: string, dependentTypes: Type array) = 6 | inherit Attribute() 7 | new (name: string) = TestNameAttribute(name, [||]) 8 | member __.Name = name 9 | member __.DependentTypeParamters = dependentTypes 10 | 11 | 12 | module Roundtrip = 13 | open System.Buffers 14 | open Expecto 15 | open Expecto.Expect 16 | open FsCheck 17 | open ProtoBuf.Meta 18 | open ProtoBuf.FSharp 19 | 20 | // F# does not allow nulls although FsCheck tries to stress C# interoperability. 21 | // Disabling it here because this library is for wrapping F# types only. 22 | type DataGenerator = 23 | // static member Generate() : Arbitrary = 24 | // Gen.oneof ([ "One"; "Two"; "" ] |> List.map Gen.constant) 25 | // |> Gen.listOf 26 | // |> Gen.map List.toArray 27 | // |> Arb.fromGen 28 | 29 | static member GenerateNonNullString() : Arbitrary = 30 | Arb.Default.StringWithoutNullChars().Generator |> Gen.map (fun x -> x.Get) |> Gen.filter (box >> Operators.isNull >> not) |> Arb.fromGen 31 | 32 | 33 | let private fsCheckConfig = { 34 | FsCheckConfig.defaultConfig with 35 | maxTest = 1000 36 | arbitrary = [ typeof ] 37 | } 38 | 39 | 40 | let private prepareModel<'t> (otherDependentRecordTypes: Type[]) = 41 | let name = sprintf "Model for %A" typeof<'t> 42 | let model = RuntimeTypeModel.Create(name) |> Serialiser.registerTypeIntoModel<'t> 43 | if otherDependentRecordTypes <> null then 44 | for dependentRecordType in otherDependentRecordTypes do 45 | Serialiser.registerRuntimeTypeIntoModel dependentRecordType model |> ignore 46 | model.CompileInPlace() 47 | model 48 | 49 | let private roundtripSerialise<'t when 't : equality> (model: RuntimeTypeModel) (valueToTest: 't) = 50 | let cloned = model.DeepClone(valueToTest) 51 | equal (unbox cloned) valueToTest "Protobuf deep clone" 52 | 53 | let rtData = 54 | let bw = ArrayBufferWriter(512) 55 | model.Serialize(bw, valueToTest) 56 | model.Deserialize<'t>(bw.WrittenSpan, Unchecked.defaultof<'t>, null) 57 | // use ms = new MemoryStream() 58 | // Serialiser.serialise model ms typeToTest 59 | // ms.Seek(0L, SeekOrigin.Begin) |> ignore 60 | // Serialiser.deserialise<'t> model ms 61 | 62 | equal rtData valueToTest "ser/des yields different result" 63 | 64 | let buildProperty<'t when 't : equality> () = 65 | let struct (testName, otherDependentRecordTypes) = 66 | match typeof<'t>.GetCustomAttributes(typeof, true) with 67 | | [| :? TestNameAttribute as attr |] -> 68 | struct (sprintf "%s (type = %A)" attr.Name typeof<'t>, attr.DependentTypeParamters) 69 | | _ -> struct (sprintf "Roundtrip for %A" typeof<'t>, Array.empty) 70 | 71 | let model = prepareModel<'t> otherDependentRecordTypes 72 | testPropertyWithConfig fsCheckConfig testName (roundtripSerialise<'t> model) 73 | 74 | let testValue<'t when 't : equality> otherDependentRecordTypes (valueToTest: 't) = 75 | let model = prepareModel<'t> otherDependentRecordTypes 76 | roundtripSerialise model valueToTest 77 | -------------------------------------------------------------------------------- /test/ProtoBuf.FSharp.Unit/Program.fs: -------------------------------------------------------------------------------- 1 | open System 2 | open Expecto 3 | 4 | 5 | [] 6 | let main argv = 7 | runTestsInAssemblyWithCLIArgs [] argv 8 | -------------------------------------------------------------------------------- /test/ProtoBuf.FSharp.Unit/ProtoBuf.FSharp.Unit.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | false 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/ProtoBuf.FSharp.Unit/TestRecordRoundtrip.fs: -------------------------------------------------------------------------------- 1 | namespace ProtoBuf.FSharp.Unit 2 | 3 | open Expecto 4 | 5 | [] 6 | type TestRecordOne = 7 | { 8 | One: string 9 | Two: int 10 | Three: string[] 11 | Four: string option 12 | } 13 | 14 | [] 15 | type TestRecordTwo = { TwoOne: string option; TwoTwo: int option } 16 | 17 | [] 18 | type TestRecordThree = { Three: string; Four: int } 19 | 20 | [] 21 | type TestRecordFour = { 22 | mutable Flag : bool 23 | String : string 24 | } 25 | 26 | 27 | [] 28 | type TestRecordFive = { 29 | Flag : bool 30 | String : string 31 | } 32 | 33 | type TestEnum = 34 | | OptionA = 1uy 35 | | OptionB = 2uy 36 | | OptionC = 69uy 37 | 38 | [] 39 | type TestRecordSix = internal { 40 | Field : struct (int * bool * int64) 41 | Number : int64 42 | DecimalNumber : decimal 43 | EnumField : TestEnum 44 | String : string 45 | Date : System.DateTime 46 | } 47 | 48 | [] 49 | type TestRecordSeven = { 50 | ``__$uperF!eld__`` : bool 51 | ``String#`` : string 52 | } 53 | 54 | type InnerNestedRecordWithCollections = { 55 | ArrayData: int array 56 | StringData: string 57 | } 58 | 59 | [ |])>] 60 | type NestedRecordWithZeroValues = { 61 | NestedCollectedData: InnerNestedRecordWithCollections array 62 | NestedData: InnerNestedRecordWithCollections 63 | Data: int array 64 | Name: string 65 | } 66 | 67 | [] 68 | type StructRecordWithCollectionTestCases = { 69 | TextCollection: string array 70 | Data: int array 71 | Name: string 72 | } 73 | 74 | [ |])>] 75 | type StructRecordWithNestedTypes = { 76 | DataCollection: InnerNestedRecordWithCollections array 77 | Data: InnerNestedRecordWithCollections 78 | } 79 | 80 | [; typeof |])>] 81 | type StructRecordWithNestedStructCollectionTypes = { 82 | StructDataCollection: StructRecordWithCollectionTestCases array 83 | Data: InnerNestedRecordWithCollections 84 | } 85 | 86 | [] 87 | type StructWith2GenericArs<'t, 'r> = { 88 | Count : int 89 | Data : 't[] 90 | Data2 : 'r 91 | } 92 | 93 | module TestRecordRoundtrip = 94 | let manualTestCases = [ 95 | testCase "Can serialise empty array, string and option" <| fun () -> Roundtrip.testValue [||] { One = ""; Two = 1; Three = [||]; Four = None } 96 | testCase "Can serialise option containing value" <| fun () -> Roundtrip.testValue [||] { One = ""; Two = 1; Three = [||]; Four = Some "TEST" } 97 | testCase "Can serialise string, array and option containing value" <| fun () -> Roundtrip.testValue [||] { One = "TEST"; Two = 1; Three = [| "TEST1" |]; Four = Some "TEST" } 98 | ] 99 | 100 | [] 101 | let test() = 102 | testList "Record Test Cases" [ 103 | testList "Manual" manualTestCases 104 | Roundtrip.buildProperty() 105 | Roundtrip.buildProperty() 106 | Roundtrip.buildProperty() 107 | Roundtrip.buildProperty() 108 | Roundtrip.buildProperty() 109 | Roundtrip.buildProperty() 110 | Roundtrip.buildProperty() 111 | Roundtrip.buildProperty() 112 | Roundtrip.buildProperty() 113 | Roundtrip.buildProperty() 114 | Roundtrip.buildProperty() 115 | Roundtrip.buildProperty>() 116 | Roundtrip.buildProperty>() 117 | Roundtrip.buildProperty>() 118 | ] 119 | -------------------------------------------------------------------------------- /test/ProtoBuf.FSharp.Unit/TestUnionRoundtrip.fs: -------------------------------------------------------------------------------- 1 | namespace ProtoBuf.FSharp.Unit 2 | 3 | open Expecto 4 | open Expecto.Expect 5 | open ProtoBuf.FSharp 6 | open ProtoBuf.Meta 7 | 8 | module ExampleTypesInsideModule = 9 | 10 | [] 11 | type UnionOne = | One 12 | 13 | [] 14 | type UnionTwo = | One | Two 15 | 16 | [] 17 | type UnionThree = | One | Two of string 18 | 19 | [] 20 | type UnionFour = | One of int | Two of string 21 | 22 | [] 23 | type UnionFive = | One of int | Two of test1: string * test2: int 24 | 25 | [] 26 | type UnionSix = | One of int | Two of test1: string * test2: int array 27 | 28 | [] 29 | type UnionSeven = | One of int option | Two of test1: int option * test2: int array 30 | 31 | [] 32 | type UnionEight = | One of int option * two: int array 33 | 34 | [] 35 | type SerialisableOption<'t> = 36 | | SerialisableSome of 't 37 | | SerialisableNone 38 | 39 | [] 40 | type Wrapper<'t> = | Wrapper of 't 41 | 42 | [] 43 | type UnionNine = 44 | | CaseOne of numbers: int array // If any of the above show it. 45 | | CaseTwo of strings: string array 46 | | CaseThreee of singleData: string 47 | | CaseFour 48 | 49 | [] 50 | type ValueUnionNoData = 51 | | CaseOne 52 | | CaseTwo 53 | | CaseThreee 54 | 55 | module TestUnionRoundtrip = 56 | // This test is just to show how the schema will be look like for other consumers. It is expected to fail so isn't used normally. 57 | let manualTest = 58 | testCase 59 | "Generate schema" 60 | (fun () -> 61 | let model = RuntimeTypeModel.Create("") |> Serialiser.registerUnionIntoModel 62 | model.CompileInPlace() 63 | let schema = model.GetSchema(typeof) 64 | equal schema "" "Schema generated") 65 | 66 | [] 67 | let test() = 68 | testList "Union Test Cases" [ //manualTest 69 | Roundtrip.buildProperty() 70 | Roundtrip.buildProperty() 71 | Roundtrip.buildProperty() 72 | Roundtrip.buildProperty() 73 | Roundtrip.buildProperty() 74 | Roundtrip.buildProperty() 75 | Roundtrip.buildProperty() 76 | Roundtrip.buildProperty() 77 | Roundtrip.buildProperty>() 78 | Roundtrip.buildProperty>() 79 | Roundtrip.buildProperty() 80 | Roundtrip.buildProperty>() 81 | Roundtrip.buildProperty>() 82 | Roundtrip.buildProperty>() 83 | Roundtrip.buildProperty() 84 | ] 85 | -------------------------------------------------------------------------------- /test/ProtoBuf.FSharp.Unit/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | True 6 | 7 | 8 | 9 | 10 | True 11 | 12 | 13 | 14 | 15 | True 16 | 17 | 18 | 19 | 20 | True 21 | 22 | 23 | 24 | 25 | True 26 | 27 | 28 | 29 | 30 | True 31 | 32 | 33 | 34 | 35 | True 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/ProtoBuf.FSharp.Unit/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | protobuf-net 3 | Expecto 4 | FsCheck 5 | Expecto.FsCheck 6 | YoloDev.Expecto.TestSdk 7 | Microsoft.NET.Test.Sdk 8 | --------------------------------------------------------------------------------