Skip to content

Commit f61e68e

Browse files
committed
Implement struct record serialization with Reflection.Emit
1 parent ee2243d commit f61e68e

File tree

3 files changed

+27
-13
lines changed

3 files changed

+27
-13
lines changed

benchmarks/FSharp.SystemTextJson.Benchmarks/Program.fs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ type TestRecord =
2222
thing: bool option
2323
time: System.DateTimeOffset }
2424

25+
[<Struct>]
26+
type TestStructRecord =
27+
{ name: string
28+
thing: bool voption
29+
time: System.DateTimeOffset }
30+
2531
type SimpleClass() =
2632
member val Name: string = null with get, set
2733
member val Thing: bool option = None with get, set
@@ -58,7 +64,7 @@ type ArrayTestBase<'t>(instance: 't) =
5864
[<Benchmark>]
5965
member this.Deserialize_SystemTextJson () = System.Text.Json.JsonSerializer.Deserialize<'t[]>(this.Serialized, systemTextOptions)
6066

61-
let recordInstance =
67+
let recordInstance : TestRecord =
6268
{ name = "sample"
6369
thing = Some true
6470
time = System.DateTimeOffset.UnixEpoch.AddDays(200.) }
@@ -67,6 +73,14 @@ let recordInstance =
6773
type Records () =
6874
inherit ArrayTestBase<TestRecord>(recordInstance)
6975

76+
let recordStructInstance : TestStructRecord =
77+
{ name = "sample"
78+
thing = ValueSome true
79+
time = System.DateTimeOffset.UnixEpoch.AddDays(200.) }
80+
81+
type StructRecords () =
82+
inherit ArrayTestBase<TestStructRecord>(recordStructInstance)
83+
7084
type Classes() =
7185
inherit ArrayTestBase<SimpleClass>(SimpleClass(Name = "sample", Thing = Some true, Time = DateTimeOffset.UnixEpoch.AddDays(200.)))
7286

@@ -106,7 +120,7 @@ let config =
106120
.With(ExecutionValidator.FailOnError)
107121

108122
let defaultSwitch () =
109-
BenchmarkSwitcher([| typeof<Records>; typeof<Classes>; typeof<ReflectionComparison> |])
123+
BenchmarkSwitcher([| typeof<Records>; typeof<StructRecords>; typeof<Classes>; typeof<ReflectionComparison> |])
110124

111125

112126
[<EntryPoint>]

src/FSharp.SystemTextJson/Record.fs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ open FSharp.Reflection
88
type JsonRecordConverter<'T>() =
99
inherit JsonConverter<'T>()
1010

11-
static let fieldProps = RecordField<'T>.properties()
11+
static let fieldProps = RecordField<'T>.fields()
1212

1313
static let expectedFieldCount =
1414
fieldProps
@@ -54,11 +54,10 @@ type JsonRecordConverter<'T>() =
5454

5555
override __.Write(writer, value, options) =
5656
writer.WriteStartObject()
57-
fieldProps
58-
|> Array.iter (fun p ->
57+
for p in fieldProps do
5958
if not p.Ignore then
6059
writer.WritePropertyName(p.Name)
61-
p.Serialize.Invoke(writer, value, options))
60+
p.Serialize.Invoke(writer, value, options)
6261
writer.WriteEndObject()
6362

6463
type JsonRecordConverter() =

src/FSharp.SystemTextJson/RecordField.fs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@ open FSharp.Reflection
66
open System.Reflection.Emit
77
open System.Text.Json
88

9-
type internal Serializer<'Record> = delegate of Utf8JsonWriter * 'Record * JsonSerializerOptions -> unit
10-
type internal FieldReader<'Record, 'Field> = delegate of 'Record -> 'Field
9+
type internal Serializer = Action<Utf8JsonWriter, obj, JsonSerializerOptions>
1110

1211
type internal RecordField<'Record> =
1312
{
1413
Name: string
1514
Type: Type
1615
Ignore: bool
17-
Serialize: Serializer<'Record>
16+
Serialize: Serializer
1817
}
1918

2019
static member name (p: PropertyInfo) =
@@ -39,15 +38,17 @@ type internal RecordField<'Record> =
3938
)
4039
let gen = dynMethod.GetILGenerator()
4140
gen.Emit(OpCodes.Ldarg_0)
41+
if f.DeclaringType.IsValueType then
42+
gen.Emit(OpCodes.Unbox, typeof<'Record>)
4243
gen.Emit(OpCodes.Ldfld, f)
4344
gen.Emit(OpCodes.Ret)
44-
dynMethod.CreateDelegate(typeof<FieldReader<'Record, 'Field>>) :?> FieldReader<'Record, 'Field>
45-
Serializer<'Record>(fun writer r options ->
45+
dynMethod.CreateDelegate(typeof<Func<obj, 'Field>>) :?> Func<obj, 'Field>
46+
Serializer(fun writer r options ->
4647
let v = getter.Invoke(r)
4748
JsonSerializer.Serialize<'Field>(writer, v, options)
4849
)
4950

50-
static member properties () =
51+
static member fields () =
5152
let recordTy = typeof<'Record>
5253
let fields = recordTy.GetFields(BindingFlags.Instance ||| BindingFlags.NonPublic)
5354
let props = FSharpType.GetRecordFields(recordTy, true)
@@ -57,7 +58,7 @@ type internal RecordField<'Record> =
5758
typeof<RecordField<'Record>>.GetMethod("serializer", BindingFlags.Static ||| BindingFlags.NonPublic)
5859
.MakeGenericMethod(p.PropertyType)
5960
.Invoke(null, [|f|])
60-
:?> Serializer<'Record>
61+
:?> Serializer
6162
{
6263
Name = RecordField<'Record>.name p
6364
Type = p.PropertyType

0 commit comments

Comments
 (0)