@dcl/protocol 1.0.0-27358757907.commit-3c70e8a → 1.0.0-27574288540.commit-f0df3e0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/package.json +6 -4
- package/protoc-gen-bitwise/generator_csharp.js +215 -0
- package/protoc-gen-bitwise/options.js +108 -0
- package/protoc-gen-bitwise/plugin.js +87 -0
- package/protoc-gen-bitwise/wire.js +239 -0
- package/protoc-gen-bitwise/generator_csharp.py +0 -176
- package/protoc-gen-bitwise/options_pb2.py +0 -171
- package/protoc-gen-bitwise/plugin.py +0 -92
package/README.md
CHANGED
|
@@ -100,13 +100,11 @@ client can read it without knowledge of the plugin.
|
|
|
100
100
|
|
|
101
101
|
| Requirement | Version |
|
|
102
102
|
|---|---|
|
|
103
|
-
|
|
|
104
|
-
| `protobuf` Python package | 4.x or 3.20+ |
|
|
103
|
+
| Node.js | 16+ |
|
|
105
104
|
| `protoc` | 3.19+ |
|
|
106
105
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
```
|
|
106
|
+
The plugin is a dependency-free Node script — no `npm install` or extra
|
|
107
|
+
packages are required to run it.
|
|
110
108
|
|
|
111
109
|
## Step 1 — Annotate your `.proto` file
|
|
112
110
|
|
|
@@ -157,11 +155,16 @@ protoc \
|
|
|
157
155
|
--proto_path=proto \
|
|
158
156
|
--proto_path=/path/to/google/protobuf/include \
|
|
159
157
|
--csharp_out=generated/cs \
|
|
160
|
-
--plugin=protoc-gen-bitwise=protoc-gen-bitwise/plugin.
|
|
158
|
+
--plugin=protoc-gen-bitwise=protoc-gen-bitwise/plugin.js \
|
|
161
159
|
--bitwise_out=generated/cs \
|
|
162
160
|
proto/decentraland/kernel/comms/v3/comms.proto
|
|
163
161
|
```
|
|
164
162
|
|
|
163
|
+
> `plugin.js` carries a `#!/usr/bin/env node` shebang. On Windows, protoc
|
|
164
|
+
> cannot exec a `.js` directly, so point `--plugin` at a small `.cmd` wrapper
|
|
165
|
+
> that runs `node plugin.js`; on unix an equivalent shell script is used. Both
|
|
166
|
+
> consumer repos (Pulse, unity-explorer) generate this wrapper automatically.
|
|
167
|
+
|
|
165
168
|
The plugin emits one `*.Bitwise.cs` file (PascalCase, flat in the output
|
|
166
169
|
directory) for each `.proto` file that contains at least one `[(quantized)]`
|
|
167
170
|
field.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcl/protocol",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-27574288540.commit-f0df3e0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"license": "Apache-2.0",
|
|
14
14
|
"main": "index.js",
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "./scripts/check-proto-compabitility.sh"
|
|
16
|
+
"test": "./scripts/check-proto-compabitility.sh",
|
|
17
|
+
"gen:test": "node protoc-gen-bitwise/test/generator.test.js"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
20
|
"@protobuf-ts/protoc": "^2.11.0",
|
|
@@ -32,7 +33,8 @@
|
|
|
32
33
|
"out-ts",
|
|
33
34
|
"out-js",
|
|
34
35
|
"public",
|
|
35
|
-
"protoc-gen-bitwise"
|
|
36
|
+
"protoc-gen-bitwise/*.js",
|
|
37
|
+
"protoc-gen-bitwise/runtime"
|
|
36
38
|
],
|
|
37
|
-
"commit": "
|
|
39
|
+
"commit": "f0df3e02ab2f989527ee9b45a1dbc85948842aab"
|
|
38
40
|
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* C# code generator for the protoc-gen-bitwise plugin.
|
|
5
|
+
*
|
|
6
|
+
* For every proto message that contains at least one uint32 field annotated
|
|
7
|
+
* with [(quantized)], this module emits a C# partial class that adds a computed
|
|
8
|
+
* float property named {FieldName}Quantized. The getter decodes the stored
|
|
9
|
+
* uint32 to a float; the setter encodes a float back to a uint32. Standard
|
|
10
|
+
* protobuf handles serialization of the uint32 wire field; this class adds a
|
|
11
|
+
* typed float accessor on top.
|
|
12
|
+
*
|
|
13
|
+
* Only uint32 fields are supported. bit_packed and unannotated fields are
|
|
14
|
+
* passed through without generating any accessor.
|
|
15
|
+
*
|
|
16
|
+
* Port of the original generator_csharp.py — output is intended to be
|
|
17
|
+
* byte-for-byte identical.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { getFieldOptions } = require('./options')
|
|
21
|
+
|
|
22
|
+
// FieldDescriptorProto type/label constants.
|
|
23
|
+
const TYPE_UINT32 = 13
|
|
24
|
+
const LABEL_REPEATED = 3
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** Mirrors Python str.capitalize(): upper-first, lowercase the rest. */
|
|
31
|
+
function capitalize(word) {
|
|
32
|
+
if (word.length === 0) return ''
|
|
33
|
+
return word[0].toUpperCase() + word.slice(1).toLowerCase()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** position_x -> PositionX */
|
|
37
|
+
function snakeToPascal(name) {
|
|
38
|
+
return name.split('_').map(capitalize).join('')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** decentraland.kernel.comms.v3 -> Decentraland.Kernel.Comms.V3 */
|
|
42
|
+
function packageToNamespace(pkg) {
|
|
43
|
+
if (!pkg) return 'Generated'
|
|
44
|
+
return pkg.split('.').map(capitalize).join('.')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format a number with C printf %g semantics at `precision` significant digits:
|
|
49
|
+
* shortest of fixed/scientific, with trailing zeros and a trailing dot removed.
|
|
50
|
+
* Reproduces Python's `f'{value:.{precision}g}'`.
|
|
51
|
+
*/
|
|
52
|
+
function formatG(value, precision) {
|
|
53
|
+
if (precision <= 0) precision = 1
|
|
54
|
+
if (value === 0) return '0'
|
|
55
|
+
if (!Number.isFinite(value)) return value > 0 ? 'inf' : 'nan'
|
|
56
|
+
|
|
57
|
+
const negative = value < 0
|
|
58
|
+
const v = Math.abs(value)
|
|
59
|
+
|
|
60
|
+
// Correctly-rounded scientific form yields the decimal exponent X (handling
|
|
61
|
+
// carry such as 9.999 -> 1.0e+1).
|
|
62
|
+
const sci = v.toExponential(precision - 1)
|
|
63
|
+
const eIdx = sci.indexOf('e')
|
|
64
|
+
const X = parseInt(sci.slice(eIdx + 1), 10)
|
|
65
|
+
|
|
66
|
+
let result
|
|
67
|
+
if (X >= -4 && X < precision) {
|
|
68
|
+
// Fixed notation with (precision - 1 - X) fraction digits.
|
|
69
|
+
const fractionDigits = precision - 1 - X
|
|
70
|
+
result = v.toFixed(fractionDigits >= 0 ? fractionDigits : 0)
|
|
71
|
+
if (result.indexOf('.') !== -1) {
|
|
72
|
+
result = result.replace(/0+$/, '').replace(/\.$/, '')
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// Scientific notation; printf prints at least two exponent digits.
|
|
76
|
+
let mantissa = sci.slice(0, eIdx)
|
|
77
|
+
if (mantissa.indexOf('.') !== -1) {
|
|
78
|
+
mantissa = mantissa.replace(/0+$/, '').replace(/\.$/, '')
|
|
79
|
+
}
|
|
80
|
+
const expSign = X < 0 ? '-' : '+'
|
|
81
|
+
let expDigits = String(Math.abs(X))
|
|
82
|
+
if (expDigits.length < 2) expDigits = '0' + expDigits
|
|
83
|
+
result = mantissa + 'e' + expSign + expDigits
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (negative ? '-' : '') + result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Format a value as a C# float literal (e.g. -100.0f). */
|
|
90
|
+
function formatFloat(value) {
|
|
91
|
+
let text = formatG(value, 8)
|
|
92
|
+
if (text.indexOf('.') === -1 && text.indexOf('e') === -1 && text.indexOf('E') === -1) {
|
|
93
|
+
text += '.0'
|
|
94
|
+
}
|
|
95
|
+
return text + 'f'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Format a quantization step size for a doc comment (e.g. "≈ 0.003"). */
|
|
99
|
+
function formatStep(step) {
|
|
100
|
+
return '≈ ' + formatG(step, 6)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Per-message code generation
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generate a C# partial class for a proto message, or null if it has no
|
|
109
|
+
* quantized uint32 fields. Returns an array of lines (no trailing newline).
|
|
110
|
+
*/
|
|
111
|
+
function generateMessage(msgProto, indent) {
|
|
112
|
+
const i = indent || ' '
|
|
113
|
+
const props = []
|
|
114
|
+
|
|
115
|
+
for (const field of msgProto.field) {
|
|
116
|
+
// Repeated/map fields are not supported.
|
|
117
|
+
if (field.label === LABEL_REPEATED) continue
|
|
118
|
+
// Only uint32 fields are candidates for quantized accessors.
|
|
119
|
+
if (field.type !== TYPE_UINT32) continue
|
|
120
|
+
|
|
121
|
+
const { quantized } = getFieldOptions(field.optionsRaw)
|
|
122
|
+
if (quantized === null) continue
|
|
123
|
+
|
|
124
|
+
const step = (quantized.max - quantized.min) / ((1 << quantized.bits) - 1)
|
|
125
|
+
|
|
126
|
+
props.push({
|
|
127
|
+
propName: snakeToPascal(field.name),
|
|
128
|
+
mn: formatFloat(quantized.min),
|
|
129
|
+
mx: formatFloat(quantized.max),
|
|
130
|
+
bits: quantized.bits,
|
|
131
|
+
step,
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (props.length === 0) return null
|
|
136
|
+
|
|
137
|
+
const backings = []
|
|
138
|
+
const lines = []
|
|
139
|
+
lines.push(`public partial class ${msgProto.name}`)
|
|
140
|
+
lines.push('{')
|
|
141
|
+
|
|
142
|
+
for (const { propName, mn, mx, bits, step } of props) {
|
|
143
|
+
const backing = '_' + propName[0].toLowerCase() + propName.slice(1)
|
|
144
|
+
backings.push(backing)
|
|
145
|
+
lines.push(`${i}private float? ${backing};`)
|
|
146
|
+
lines.push(
|
|
147
|
+
`${i}/// <summary>Float accessor for <see cref="${propName}"/>. Range [${mn}, ${mx}], ${bits} bits, step ${formatStep(step)}.</summary>`,
|
|
148
|
+
)
|
|
149
|
+
lines.push(`${i}public float ${propName}Quantized`)
|
|
150
|
+
lines.push(`${i}{`)
|
|
151
|
+
lines.push(`${i}${i}get => ${backing} ??= Quantize.Decode(${propName}, ${mn}, ${mx}, ${bits});`)
|
|
152
|
+
lines.push(`${i}${i}set { ${backing} = value; ${propName} = Quantize.Encode(value, ${mn}, ${mx}, ${bits}); }`)
|
|
153
|
+
lines.push(`${i}}`)
|
|
154
|
+
lines.push('')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
lines.push(`${i}/// <summary>Clears all cached decoded values. Call after mutating raw uint32 fields directly.</summary>`)
|
|
158
|
+
lines.push(`${i}public void ResetDecodedCache()`)
|
|
159
|
+
lines.push(`${i}{`)
|
|
160
|
+
for (const backing of backings) {
|
|
161
|
+
lines.push(`${i}${i}${backing} = null;`)
|
|
162
|
+
}
|
|
163
|
+
lines.push(`${i}}`)
|
|
164
|
+
|
|
165
|
+
lines.push('}')
|
|
166
|
+
return lines
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Per-file code generation (public entry point)
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Generate a C# source file for a FileDescriptorProto, or null if the file
|
|
175
|
+
* contains no quantized uint32 fields.
|
|
176
|
+
* @returns {{name: string, content: string} | null}
|
|
177
|
+
*/
|
|
178
|
+
function generateCsharp(fileProto) {
|
|
179
|
+
const namespace = packageToNamespace(fileProto.package)
|
|
180
|
+
|
|
181
|
+
const protoFile = fileProto.name.split('/').pop()
|
|
182
|
+
const stem = snakeToPascal(protoFile.replace('.proto', ''))
|
|
183
|
+
const outName = `${stem}.Bitwise.cs`
|
|
184
|
+
|
|
185
|
+
const header = [
|
|
186
|
+
'// <auto-generated>',
|
|
187
|
+
'// Generated by protoc-gen-bitwise. DO NOT EDIT.',
|
|
188
|
+
`// Source: ${fileProto.name}`,
|
|
189
|
+
'// </auto-generated>',
|
|
190
|
+
'',
|
|
191
|
+
'using Decentraland.Networking.Bitwise;',
|
|
192
|
+
'',
|
|
193
|
+
`namespace ${namespace}`,
|
|
194
|
+
'{',
|
|
195
|
+
]
|
|
196
|
+
const footer = ['', `} // namespace ${namespace}`]
|
|
197
|
+
|
|
198
|
+
const body = []
|
|
199
|
+
for (const msg of fileProto.messageType) {
|
|
200
|
+
const msgLines = generateMessage(msg)
|
|
201
|
+
if (msgLines === null) continue
|
|
202
|
+
// Indent each line by 4 spaces (inside the namespace block).
|
|
203
|
+
for (const line of msgLines) {
|
|
204
|
+
body.push(line ? ' ' + line : '')
|
|
205
|
+
}
|
|
206
|
+
body.push('')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (body.length === 0) return null
|
|
210
|
+
|
|
211
|
+
const content = header.concat(body, footer).join('\n') + '\n'
|
|
212
|
+
return { name: outName, content }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = { generateCsharp }
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parser for the custom bitwise field options defined in options.proto.
|
|
5
|
+
*
|
|
6
|
+
* The descriptor decoder hands us the raw serialized FieldOptions bytes (it
|
|
7
|
+
* declares `options` as opaque bytes). We walk those bytes looking for the two
|
|
8
|
+
* custom extension field numbers — protobuf preserves unknown/unregistered
|
|
9
|
+
* extension bytes, so they are always present even though no runtime here knows
|
|
10
|
+
* the extension schema. This mirrors the original options_pb2.py.
|
|
11
|
+
*
|
|
12
|
+
* Wire format: tag = (field_number << 3) | wire_type
|
|
13
|
+
* wire_type 0 = varint, 1 = 64-bit, 2 = length-delimited, 5 = 32-bit
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { readVarint, skipField } = require('./wire')
|
|
17
|
+
|
|
18
|
+
// Extension field numbers as defined in options.proto.
|
|
19
|
+
const QUANTIZED_FIELD_NUMBER = 50001
|
|
20
|
+
const BIT_PACKED_FIELD_NUMBER = 50002
|
|
21
|
+
|
|
22
|
+
/** Parse a serialized QuantizedFloatOptions message: { min, max, bits }. */
|
|
23
|
+
function parseQuantized(data) {
|
|
24
|
+
const opts = { min: 0.0, max: 0.0, bits: 0 }
|
|
25
|
+
let pos = 0
|
|
26
|
+
while (pos < data.length) {
|
|
27
|
+
let tag
|
|
28
|
+
;[tag, pos] = readVarint(data, pos)
|
|
29
|
+
const fieldNum = tag >>> 3
|
|
30
|
+
const wireType = tag & 0x7
|
|
31
|
+
if (fieldNum === 1 && wireType === 5) {
|
|
32
|
+
// min (float)
|
|
33
|
+
opts.min = data.readFloatLE(pos)
|
|
34
|
+
pos += 4
|
|
35
|
+
} else if (fieldNum === 2 && wireType === 5) {
|
|
36
|
+
// max (float)
|
|
37
|
+
opts.max = data.readFloatLE(pos)
|
|
38
|
+
pos += 4
|
|
39
|
+
} else if (fieldNum === 3 && wireType === 0) {
|
|
40
|
+
// bits (uint32)
|
|
41
|
+
;[opts.bits, pos] = readVarint(data, pos)
|
|
42
|
+
} else {
|
|
43
|
+
pos = skipField(data, pos, wireType)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return opts
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Parse a serialized BitPackedOptions message: { bits }. */
|
|
50
|
+
function parseBitPacked(data) {
|
|
51
|
+
const opts = { bits: 0 }
|
|
52
|
+
let pos = 0
|
|
53
|
+
while (pos < data.length) {
|
|
54
|
+
let tag
|
|
55
|
+
;[tag, pos] = readVarint(data, pos)
|
|
56
|
+
const fieldNum = tag >>> 3
|
|
57
|
+
const wireType = tag & 0x7
|
|
58
|
+
if (fieldNum === 1 && wireType === 0) {
|
|
59
|
+
// bits (uint32)
|
|
60
|
+
;[opts.bits, pos] = readVarint(data, pos)
|
|
61
|
+
} else {
|
|
62
|
+
pos = skipField(data, pos, wireType)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return opts
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extract custom bitwise options from raw FieldOptions bytes.
|
|
70
|
+
*
|
|
71
|
+
* @param {Buffer|null} optionsRaw serialized FieldOptions, or null when unset.
|
|
72
|
+
* @returns {{quantized: object|null, bitPacked: object|null}}
|
|
73
|
+
*/
|
|
74
|
+
function getFieldOptions(optionsRaw) {
|
|
75
|
+
if (!optionsRaw || optionsRaw.length === 0) {
|
|
76
|
+
return { quantized: null, bitPacked: null }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let quantized = null
|
|
80
|
+
let bitPacked = null
|
|
81
|
+
let pos = 0
|
|
82
|
+
|
|
83
|
+
while (pos < optionsRaw.length) {
|
|
84
|
+
let tag
|
|
85
|
+
;[tag, pos] = readVarint(optionsRaw, pos)
|
|
86
|
+
const fieldNum = tag >>> 3
|
|
87
|
+
const wireType = tag & 0x7
|
|
88
|
+
|
|
89
|
+
if (wireType === 2) {
|
|
90
|
+
let len
|
|
91
|
+
;[len, pos] = readVarint(optionsRaw, pos)
|
|
92
|
+
const valueBytes = optionsRaw.subarray(pos, pos + len)
|
|
93
|
+
pos += len
|
|
94
|
+
if (fieldNum === QUANTIZED_FIELD_NUMBER) {
|
|
95
|
+
quantized = parseQuantized(valueBytes)
|
|
96
|
+
} else if (fieldNum === BIT_PACKED_FIELD_NUMBER) {
|
|
97
|
+
bitPacked = parseBitPacked(valueBytes)
|
|
98
|
+
}
|
|
99
|
+
// else: unknown length-delimited field — already consumed.
|
|
100
|
+
} else {
|
|
101
|
+
pos = skipField(optionsRaw, pos, wireType)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { quantized, bitPacked }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { getFieldOptions }
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* protoc-gen-bitwise — protoc plugin that generates C# bitwise serialization code.
|
|
6
|
+
*
|
|
7
|
+
* Protocol:
|
|
8
|
+
* 1. protoc writes a serialised CodeGeneratorRequest to this process's stdin.
|
|
9
|
+
* 2. This plugin reads it, generates C# partial classes with float accessor
|
|
10
|
+
* properties for every message that carries [(quantized)] field annotations.
|
|
11
|
+
* 3. A serialised CodeGeneratorResponse is written to stdout.
|
|
12
|
+
*
|
|
13
|
+
* Implemented in plain Node with a self-contained protobuf wire codec (see
|
|
14
|
+
* wire.js) so it runs with only `node` on PATH — no npm install required, even
|
|
15
|
+
* when invoked directly from a sibling checkout.
|
|
16
|
+
*
|
|
17
|
+
* Usage (from project root):
|
|
18
|
+
* protoc \
|
|
19
|
+
* --proto_path=proto \
|
|
20
|
+
* --bitwise_out=generated/ \
|
|
21
|
+
* --plugin=protoc-gen-bitwise=protoc-gen-bitwise/plugin.js \
|
|
22
|
+
* proto/my_messages.proto
|
|
23
|
+
*
|
|
24
|
+
* On Windows, protoc needs an executable wrapper, e.g. a .cmd that runs:
|
|
25
|
+
* node "<path>\protoc-gen-bitwise\plugin.js" %*
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const { decodeRequest, encodeResponse } = require('./wire')
|
|
29
|
+
const { generateCsharp } = require('./generator_csharp')
|
|
30
|
+
|
|
31
|
+
// CodeGeneratorResponse.Feature.FEATURE_PROTO3_OPTIONAL — advertised so protoc
|
|
32
|
+
// does not reject the plugin when the schema uses proto3 `optional` fields.
|
|
33
|
+
const FEATURE_PROTO3_OPTIONAL = 1
|
|
34
|
+
|
|
35
|
+
function readStdin() {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const chunks = []
|
|
38
|
+
process.stdin.on('data', (chunk) => chunks.push(chunk))
|
|
39
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks)))
|
|
40
|
+
process.stdin.on('error', reject)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
const requestBytes = await readStdin()
|
|
46
|
+
const request = decodeRequest(requestBytes)
|
|
47
|
+
|
|
48
|
+
// Lookup map for all file descriptors (parity with the Python plugin).
|
|
49
|
+
const fileByName = new Map()
|
|
50
|
+
for (const file of request.protoFile) fileByName.set(file.name, file)
|
|
51
|
+
|
|
52
|
+
const files = []
|
|
53
|
+
let error = null
|
|
54
|
+
|
|
55
|
+
for (const fileName of request.fileToGenerate) {
|
|
56
|
+
// Skip the options definition file itself — it has no messages to generate.
|
|
57
|
+
if (fileName === 'decentraland/common/options.proto') continue
|
|
58
|
+
|
|
59
|
+
const fileProto = fileByName.get(fileName)
|
|
60
|
+
if (!fileProto) continue
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const generated = generateCsharp(fileProto)
|
|
64
|
+
if (generated) files.push(generated)
|
|
65
|
+
} catch (exc) {
|
|
66
|
+
error = `protoc-gen-bitwise: error processing ${fileName}: ${exc && exc.message ? exc.message : exc}`
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const response = encodeResponse({
|
|
71
|
+
error,
|
|
72
|
+
supportedFeatures: FEATURE_PROTO3_OPTIONAL,
|
|
73
|
+
files,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Let the write flush before the process exits (do not call process.exit()).
|
|
77
|
+
process.stdout.write(response)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
main().catch((exc) => {
|
|
81
|
+
const response = encodeResponse({
|
|
82
|
+
error: `protoc-gen-bitwise: ${exc && exc.stack ? exc.stack : exc}`,
|
|
83
|
+
supportedFeatures: FEATURE_PROTO3_OPTIONAL,
|
|
84
|
+
files: [],
|
|
85
|
+
})
|
|
86
|
+
process.stdout.write(response)
|
|
87
|
+
})
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Self-contained protobuf wire-format codec for protoc-gen-bitwise.
|
|
5
|
+
*
|
|
6
|
+
* The plugin only needs a tiny slice of the descriptor/plugin schemas, so
|
|
7
|
+
* rather than pull in a protobuf runtime (which would force every consumer —
|
|
8
|
+
* including the sibling Pulse checkout that runs this file directly off disk —
|
|
9
|
+
* to `npm install` the protocol repo) we decode the handful of fields we care
|
|
10
|
+
* about by walking the wire format directly. This mirrors the original Python
|
|
11
|
+
* plugin, which already hand-parsed FieldOptions for the same reason.
|
|
12
|
+
*
|
|
13
|
+
* Decoded subset:
|
|
14
|
+
* CodeGeneratorRequest { file_to_generate = 1; proto_file = 15; }
|
|
15
|
+
* FileDescriptorProto { name = 1; package = 2; message_type = 4; }
|
|
16
|
+
* DescriptorProto { name = 1; field = 2; }
|
|
17
|
+
* FieldDescriptorProto { name = 1; label = 4; type = 5; options = 8 (raw bytes); }
|
|
18
|
+
*
|
|
19
|
+
* Encoded subset:
|
|
20
|
+
* CodeGeneratorResponse { error = 1; supported_features = 2; file = 15; }
|
|
21
|
+
* CodeGeneratorResponse.File { name = 1; content = 15; }
|
|
22
|
+
*
|
|
23
|
+
* Wire types: 0 = varint, 1 = 64-bit, 2 = length-delimited, 5 = 32-bit.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Low-level readers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** Decode a protobuf varint at `pos`. Returns [value, newPos]. */
|
|
31
|
+
function readVarint(buf, pos) {
|
|
32
|
+
let result = 0
|
|
33
|
+
let shift = 0
|
|
34
|
+
let byte
|
|
35
|
+
do {
|
|
36
|
+
byte = buf[pos++]
|
|
37
|
+
// Multiplication (not <<) keeps values correct past 32 bits.
|
|
38
|
+
result += (byte & 0x7f) * Math.pow(2, shift)
|
|
39
|
+
shift += 7
|
|
40
|
+
} while (byte & 0x80)
|
|
41
|
+
return [result, pos]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Advance `pos` past a field of the given wire type. Returns newPos. */
|
|
45
|
+
function skipField(buf, pos, wireType) {
|
|
46
|
+
switch (wireType) {
|
|
47
|
+
case 0: // varint
|
|
48
|
+
return readVarint(buf, pos)[1]
|
|
49
|
+
case 1: // 64-bit
|
|
50
|
+
return pos + 8
|
|
51
|
+
case 2: {
|
|
52
|
+
// length-delimited
|
|
53
|
+
const [length, next] = readVarint(buf, pos)
|
|
54
|
+
return next + length
|
|
55
|
+
}
|
|
56
|
+
case 5: // 32-bit
|
|
57
|
+
return pos + 4
|
|
58
|
+
default:
|
|
59
|
+
// Groups (3/4) are deprecated and never appear in descriptors.
|
|
60
|
+
return pos
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Request decoding
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
function decodeFieldDescriptor(buf) {
|
|
69
|
+
const field = { name: '', label: 0, type: 0, optionsRaw: null }
|
|
70
|
+
let pos = 0
|
|
71
|
+
while (pos < buf.length) {
|
|
72
|
+
let tag
|
|
73
|
+
;[tag, pos] = readVarint(buf, pos)
|
|
74
|
+
const fieldNum = tag >>> 3
|
|
75
|
+
const wireType = tag & 0x7
|
|
76
|
+
if (fieldNum === 1 && wireType === 2) {
|
|
77
|
+
let len
|
|
78
|
+
;[len, pos] = readVarint(buf, pos)
|
|
79
|
+
field.name = buf.toString('utf8', pos, pos + len)
|
|
80
|
+
pos += len
|
|
81
|
+
} else if (fieldNum === 4 && wireType === 0) {
|
|
82
|
+
;[field.label, pos] = readVarint(buf, pos)
|
|
83
|
+
} else if (fieldNum === 5 && wireType === 0) {
|
|
84
|
+
;[field.type, pos] = readVarint(buf, pos)
|
|
85
|
+
} else if (fieldNum === 8 && wireType === 2) {
|
|
86
|
+
let len
|
|
87
|
+
;[len, pos] = readVarint(buf, pos)
|
|
88
|
+
// Keep the raw FieldOptions bytes so custom extensions survive (the
|
|
89
|
+
// descriptor schema doesn't know about ext 50001/50002).
|
|
90
|
+
field.optionsRaw = buf.subarray(pos, pos + len)
|
|
91
|
+
pos += len
|
|
92
|
+
} else {
|
|
93
|
+
pos = skipField(buf, pos, wireType)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return field
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function decodeDescriptor(buf) {
|
|
100
|
+
const message = { name: '', field: [] }
|
|
101
|
+
let pos = 0
|
|
102
|
+
while (pos < buf.length) {
|
|
103
|
+
let tag
|
|
104
|
+
;[tag, pos] = readVarint(buf, pos)
|
|
105
|
+
const fieldNum = tag >>> 3
|
|
106
|
+
const wireType = tag & 0x7
|
|
107
|
+
if (fieldNum === 1 && wireType === 2) {
|
|
108
|
+
let len
|
|
109
|
+
;[len, pos] = readVarint(buf, pos)
|
|
110
|
+
message.name = buf.toString('utf8', pos, pos + len)
|
|
111
|
+
pos += len
|
|
112
|
+
} else if (fieldNum === 2 && wireType === 2) {
|
|
113
|
+
let len
|
|
114
|
+
;[len, pos] = readVarint(buf, pos)
|
|
115
|
+
message.field.push(decodeFieldDescriptor(buf.subarray(pos, pos + len)))
|
|
116
|
+
pos += len
|
|
117
|
+
} else {
|
|
118
|
+
// nested_type / enum_type / etc. are intentionally ignored — the
|
|
119
|
+
// generator only walks top-level messages, matching the Python plugin.
|
|
120
|
+
pos = skipField(buf, pos, wireType)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return message
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function decodeFileDescriptor(buf) {
|
|
127
|
+
const file = { name: '', package: '', messageType: [] }
|
|
128
|
+
let pos = 0
|
|
129
|
+
while (pos < buf.length) {
|
|
130
|
+
let tag
|
|
131
|
+
;[tag, pos] = readVarint(buf, pos)
|
|
132
|
+
const fieldNum = tag >>> 3
|
|
133
|
+
const wireType = tag & 0x7
|
|
134
|
+
if (fieldNum === 1 && wireType === 2) {
|
|
135
|
+
let len
|
|
136
|
+
;[len, pos] = readVarint(buf, pos)
|
|
137
|
+
file.name = buf.toString('utf8', pos, pos + len)
|
|
138
|
+
pos += len
|
|
139
|
+
} else if (fieldNum === 2 && wireType === 2) {
|
|
140
|
+
let len
|
|
141
|
+
;[len, pos] = readVarint(buf, pos)
|
|
142
|
+
file.package = buf.toString('utf8', pos, pos + len)
|
|
143
|
+
pos += len
|
|
144
|
+
} else if (fieldNum === 4 && wireType === 2) {
|
|
145
|
+
let len
|
|
146
|
+
;[len, pos] = readVarint(buf, pos)
|
|
147
|
+
file.messageType.push(decodeDescriptor(buf.subarray(pos, pos + len)))
|
|
148
|
+
pos += len
|
|
149
|
+
} else {
|
|
150
|
+
pos = skipField(buf, pos, wireType)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return file
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Decode a serialized CodeGeneratorRequest. */
|
|
157
|
+
function decodeRequest(buf) {
|
|
158
|
+
const request = { fileToGenerate: [], protoFile: [] }
|
|
159
|
+
let pos = 0
|
|
160
|
+
while (pos < buf.length) {
|
|
161
|
+
let tag
|
|
162
|
+
;[tag, pos] = readVarint(buf, pos)
|
|
163
|
+
const fieldNum = tag >>> 3
|
|
164
|
+
const wireType = tag & 0x7
|
|
165
|
+
if (fieldNum === 1 && wireType === 2) {
|
|
166
|
+
let len
|
|
167
|
+
;[len, pos] = readVarint(buf, pos)
|
|
168
|
+
request.fileToGenerate.push(buf.toString('utf8', pos, pos + len))
|
|
169
|
+
pos += len
|
|
170
|
+
} else if (fieldNum === 15 && wireType === 2) {
|
|
171
|
+
let len
|
|
172
|
+
;[len, pos] = readVarint(buf, pos)
|
|
173
|
+
request.protoFile.push(decodeFileDescriptor(buf.subarray(pos, pos + len)))
|
|
174
|
+
pos += len
|
|
175
|
+
} else {
|
|
176
|
+
pos = skipField(buf, pos, wireType)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return request
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Response encoding
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/** Encode an unsigned integer as a protobuf varint Buffer. */
|
|
187
|
+
function encodeVarint(value) {
|
|
188
|
+
const bytes = []
|
|
189
|
+
let v = value
|
|
190
|
+
while (v > 0x7f) {
|
|
191
|
+
bytes.push((v & 0x7f) | 0x80)
|
|
192
|
+
v = Math.floor(v / 128)
|
|
193
|
+
}
|
|
194
|
+
bytes.push(v & 0x7f)
|
|
195
|
+
return Buffer.from(bytes)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function encodeTag(fieldNum, wireType) {
|
|
199
|
+
return encodeVarint((fieldNum << 3) | wireType)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Encode a length-delimited (string/bytes) field: tag + length + payload. */
|
|
203
|
+
function encodeLengthDelimited(fieldNum, payload) {
|
|
204
|
+
const buf = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, 'utf8')
|
|
205
|
+
return Buffer.concat([encodeTag(fieldNum, 2), encodeVarint(buf.length), buf])
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function encodeFile(file) {
|
|
209
|
+
return Buffer.concat([
|
|
210
|
+
encodeLengthDelimited(1, file.name), // name = 1
|
|
211
|
+
encodeLengthDelimited(15, file.content), // content = 15
|
|
212
|
+
])
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Encode a CodeGeneratorResponse.
|
|
217
|
+
* @param {{error?: string|null, supportedFeatures?: number, files?: Array<{name:string,content:string}>}} response
|
|
218
|
+
*/
|
|
219
|
+
function encodeResponse(response) {
|
|
220
|
+
const parts = []
|
|
221
|
+
if (response.error != null) {
|
|
222
|
+
parts.push(encodeLengthDelimited(1, response.error)) // error = 1
|
|
223
|
+
}
|
|
224
|
+
if (response.supportedFeatures != null) {
|
|
225
|
+
parts.push(encodeTag(2, 0)) // supported_features = 2 (varint)
|
|
226
|
+
parts.push(encodeVarint(response.supportedFeatures))
|
|
227
|
+
}
|
|
228
|
+
for (const file of response.files || []) {
|
|
229
|
+
parts.push(encodeLengthDelimited(15, encodeFile(file))) // file = 15
|
|
230
|
+
}
|
|
231
|
+
return Buffer.concat(parts)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
readVarint,
|
|
236
|
+
skipField,
|
|
237
|
+
decodeRequest,
|
|
238
|
+
encodeResponse,
|
|
239
|
+
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
C# code generator for the protoc-gen-bitwise plugin.
|
|
3
|
-
|
|
4
|
-
For every proto message that contains at least one uint32 field annotated with
|
|
5
|
-
[(quantized)], this module emits a C# partial class that adds a computed float
|
|
6
|
-
property named {FieldName}Quantized. The getter decodes the stored uint32 to
|
|
7
|
-
a float; the setter encodes a float back to a uint32. Standard protobuf
|
|
8
|
-
handles serialization of the uint32 wire field; this class adds a typed float
|
|
9
|
-
accessor on top.
|
|
10
|
-
|
|
11
|
-
Protobuf encodes small uint32 values via varint, so a value that fits in
|
|
12
|
-
2^20 costs 3 bytes on the wire — cheaper than a raw IEEE 754 float (4 bytes).
|
|
13
|
-
|
|
14
|
-
Only uint32 fields are supported. bit_packed and unannotated fields are
|
|
15
|
-
passed through without generating any accessor.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
from google.protobuf import descriptor_pb2
|
|
19
|
-
|
|
20
|
-
from options_pb2 import get_field_options
|
|
21
|
-
|
|
22
|
-
# FieldDescriptorProto type constants (aliased for readability)
|
|
23
|
-
_FT = descriptor_pb2.FieldDescriptorProto
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# ---------------------------------------------------------------------------
|
|
27
|
-
# Helpers
|
|
28
|
-
# ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
def _snake_to_pascal(name: str) -> str:
|
|
31
|
-
"""position_x → PositionX"""
|
|
32
|
-
return ''.join(word.capitalize() for word in name.split('_'))
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _package_to_namespace(package: str) -> str:
|
|
36
|
-
"""decentraland.kernel.comms.v3 → Decentraland.Kernel.Comms.V3"""
|
|
37
|
-
if not package:
|
|
38
|
-
return 'Generated'
|
|
39
|
-
return '.'.join(part.capitalize() for part in package.split('.'))
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _format_float(value: float) -> str:
|
|
43
|
-
"""Format a Python float as a C# float literal (e.g. -100.0f)."""
|
|
44
|
-
text = f'{value:.8g}'
|
|
45
|
-
if '.' not in text and 'e' not in text and 'E' not in text:
|
|
46
|
-
text += '.0'
|
|
47
|
-
return text + 'f'
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _format_step(step: float) -> str:
|
|
51
|
-
"""Format a quantization step size for display in a doc comment (e.g. ≈ 0.003)."""
|
|
52
|
-
return f'\u2248 {step:.6g}'
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# ---------------------------------------------------------------------------
|
|
56
|
-
# Per-message code generation
|
|
57
|
-
# ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
def _gen_message(msg_proto, indent: str = ' ') -> list[str] | None:
|
|
60
|
-
"""
|
|
61
|
-
Generate a C# partial class for a proto message.
|
|
62
|
-
|
|
63
|
-
For each uint32 field with a [(quantized)] annotation, emits a cached float
|
|
64
|
-
property {FieldName}Quantized backed by the raw uint32 field.
|
|
65
|
-
|
|
66
|
-
Returns a list of lines (without trailing newline) or None if the message
|
|
67
|
-
has no quantized uint32 fields.
|
|
68
|
-
"""
|
|
69
|
-
props: list[tuple[str, str, str, int, float]] = [] # (prop_name, mn, mx, bits, step)
|
|
70
|
-
|
|
71
|
-
for field in msg_proto.field:
|
|
72
|
-
# Repeated/map fields are not supported
|
|
73
|
-
if field.label == _FT.LABEL_REPEATED:
|
|
74
|
-
continue
|
|
75
|
-
|
|
76
|
-
# Only uint32 fields are candidates for quantized accessors
|
|
77
|
-
if field.type != _FT.TYPE_UINT32:
|
|
78
|
-
continue
|
|
79
|
-
|
|
80
|
-
quantized, _ = get_field_options(field.options)
|
|
81
|
-
if quantized is None:
|
|
82
|
-
continue
|
|
83
|
-
|
|
84
|
-
step = (quantized.max - quantized.min) / ((1 << quantized.bits) - 1)
|
|
85
|
-
|
|
86
|
-
props.append((
|
|
87
|
-
_snake_to_pascal(field.name),
|
|
88
|
-
_format_float(quantized.min),
|
|
89
|
-
_format_float(quantized.max),
|
|
90
|
-
quantized.bits,
|
|
91
|
-
step,
|
|
92
|
-
))
|
|
93
|
-
|
|
94
|
-
if not props:
|
|
95
|
-
return None
|
|
96
|
-
|
|
97
|
-
i = indent
|
|
98
|
-
backings: list[str] = []
|
|
99
|
-
lines: list[str] = []
|
|
100
|
-
lines.append(f'public partial class {msg_proto.name}')
|
|
101
|
-
lines.append('{')
|
|
102
|
-
|
|
103
|
-
for prop_name, mn, mx, bits, step in props:
|
|
104
|
-
backing = '_' + prop_name[0].lower() + prop_name[1:]
|
|
105
|
-
backings.append(backing)
|
|
106
|
-
lines.append(f'{i}private float? {backing};')
|
|
107
|
-
lines.append(f'{i}/// <summary>Float accessor for <see cref="{prop_name}"/>. Range [{mn}, {mx}], {bits} bits, step {_format_step(step)}.</summary>')
|
|
108
|
-
lines.append(f'{i}public float {prop_name}Quantized')
|
|
109
|
-
lines.append(f'{i}{{')
|
|
110
|
-
lines.append(f'{i}{i}get => {backing} ??= Quantize.Decode({prop_name}, {mn}, {mx}, {bits});')
|
|
111
|
-
lines.append(f'{i}{i}set {{ {backing} = value; {prop_name} = Quantize.Encode(value, {mn}, {mx}, {bits}); }}')
|
|
112
|
-
lines.append(f'{i}}}')
|
|
113
|
-
lines.append('')
|
|
114
|
-
|
|
115
|
-
lines.append(f'{i}/// <summary>Clears all cached decoded values. Call after mutating raw uint32 fields directly.</summary>')
|
|
116
|
-
lines.append(f'{i}public void ResetDecodedCache()')
|
|
117
|
-
lines.append(f'{i}{{')
|
|
118
|
-
for backing in backings:
|
|
119
|
-
lines.append(f'{i}{i}{backing} = null;')
|
|
120
|
-
lines.append(f'{i}}}')
|
|
121
|
-
|
|
122
|
-
lines.append('}')
|
|
123
|
-
return lines
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
# ---------------------------------------------------------------------------
|
|
127
|
-
# Per-file code generation (public entry point)
|
|
128
|
-
# ---------------------------------------------------------------------------
|
|
129
|
-
|
|
130
|
-
def generate_csharp(file_proto) -> dict | None:
|
|
131
|
-
"""
|
|
132
|
-
Generate a C# source file for a FileDescriptorProto.
|
|
133
|
-
|
|
134
|
-
Returns a dict with keys 'name' (output path) and 'content' (C# source),
|
|
135
|
-
or None if the file contains no quantized uint32 fields.
|
|
136
|
-
"""
|
|
137
|
-
namespace = _package_to_namespace(file_proto.package)
|
|
138
|
-
|
|
139
|
-
# Output is placed flat in the output root, matching --csharp_out convention.
|
|
140
|
-
# e.g. "decentraland/kernel/comms/v3/my_message.proto"
|
|
141
|
-
# → "MyMessage.Bitwise.cs"
|
|
142
|
-
proto_file = file_proto.name.rsplit('/', 1)[-1]
|
|
143
|
-
stem = _snake_to_pascal(proto_file.replace('.proto', ''))
|
|
144
|
-
out_name = f'{stem}.Bitwise.cs'
|
|
145
|
-
|
|
146
|
-
header = [
|
|
147
|
-
'// <auto-generated>',
|
|
148
|
-
'// Generated by protoc-gen-bitwise. DO NOT EDIT.',
|
|
149
|
-
f'// Source: {file_proto.name}',
|
|
150
|
-
'// </auto-generated>',
|
|
151
|
-
'',
|
|
152
|
-
'using Decentraland.Networking.Bitwise;',
|
|
153
|
-
'',
|
|
154
|
-
f'namespace {namespace}',
|
|
155
|
-
'{',
|
|
156
|
-
]
|
|
157
|
-
footer = [
|
|
158
|
-
'',
|
|
159
|
-
f'}} // namespace {namespace}',
|
|
160
|
-
]
|
|
161
|
-
|
|
162
|
-
body: list[str] = []
|
|
163
|
-
for msg in file_proto.message_type:
|
|
164
|
-
msg_lines = _gen_message(msg)
|
|
165
|
-
if msg_lines is None:
|
|
166
|
-
continue
|
|
167
|
-
# Indent each line by 4 spaces (inside the namespace block)
|
|
168
|
-
for line in msg_lines:
|
|
169
|
-
body.append((' ' + line) if line else '')
|
|
170
|
-
body.append('')
|
|
171
|
-
|
|
172
|
-
if not body:
|
|
173
|
-
return None # nothing to emit
|
|
174
|
-
|
|
175
|
-
content = '\n'.join(header + body + footer) + '\n'
|
|
176
|
-
return {'name': out_name, 'content': content}
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Manual parser for the custom bitwise field options defined in options.proto.
|
|
3
|
-
|
|
4
|
-
Rather than relying on protobuf extension registration (which requires a properly
|
|
5
|
-
compiled _pb2 module), this module parses the raw serialized FieldOptions bytes
|
|
6
|
-
directly using the protobuf binary wire format. All protobuf runtimes preserve
|
|
7
|
-
unknown/unregistered extension bytes when round-tripping, so
|
|
8
|
-
field.options.SerializeToString() always contains the extension data even when
|
|
9
|
-
the extension is not registered in the Python runtime.
|
|
10
|
-
|
|
11
|
-
Wire format reference:
|
|
12
|
-
tag = (field_number << 3) | wire_type
|
|
13
|
-
wire_type 0 = varint, 1 = 64-bit, 2 = length-delimited, 5 = 32-bit
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
import struct
|
|
17
|
-
|
|
18
|
-
# Extension field numbers as defined in options.proto
|
|
19
|
-
QUANTIZED_FIELD_NUMBER = 50001
|
|
20
|
-
BIT_PACKED_FIELD_NUMBER = 50002
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# ---------------------------------------------------------------------------
|
|
24
|
-
# Low-level wire-format helpers
|
|
25
|
-
# ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
def _read_varint(data: bytes, pos: int):
|
|
28
|
-
"""Decode a protobuf varint starting at *pos*. Returns (value, new_pos)."""
|
|
29
|
-
result = 0
|
|
30
|
-
shift = 0
|
|
31
|
-
while pos < len(data):
|
|
32
|
-
byte = data[pos]
|
|
33
|
-
pos += 1
|
|
34
|
-
result |= (byte & 0x7F) << shift
|
|
35
|
-
if not (byte & 0x80):
|
|
36
|
-
break
|
|
37
|
-
shift += 7
|
|
38
|
-
return result, pos
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _read_float32(data: bytes, pos: int):
|
|
42
|
-
"""Decode a little-endian 32-bit float. Returns (value, new_pos)."""
|
|
43
|
-
value, = struct.unpack_from('<f', data, pos)
|
|
44
|
-
return value, pos + 4
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _skip_field(data: bytes, pos: int, wire_type: int) -> int:
|
|
48
|
-
"""Advance *pos* past a field with the given wire_type."""
|
|
49
|
-
if wire_type == 0:
|
|
50
|
-
_, pos = _read_varint(data, pos)
|
|
51
|
-
elif wire_type == 1:
|
|
52
|
-
pos += 8
|
|
53
|
-
elif wire_type == 2:
|
|
54
|
-
length, pos = _read_varint(data, pos)
|
|
55
|
-
pos += length
|
|
56
|
-
elif wire_type == 5:
|
|
57
|
-
pos += 4
|
|
58
|
-
# wire types 3 and 4 (start/end group) are deprecated; skip gracefully
|
|
59
|
-
return pos
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# ---------------------------------------------------------------------------
|
|
63
|
-
# Option message classes
|
|
64
|
-
# ---------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
class QuantizedFloatOptions:
|
|
67
|
-
"""Mirrors the QuantizedFloatOptions proto message."""
|
|
68
|
-
|
|
69
|
-
__slots__ = ('min', 'max', 'bits')
|
|
70
|
-
|
|
71
|
-
def __init__(self, min_val: float = 0.0, max_val: float = 0.0, bits: int = 0):
|
|
72
|
-
self.min = min_val
|
|
73
|
-
self.max = max_val
|
|
74
|
-
self.bits = bits
|
|
75
|
-
|
|
76
|
-
@classmethod
|
|
77
|
-
def from_bytes(cls, data: bytes) -> 'QuantizedFloatOptions':
|
|
78
|
-
obj = cls()
|
|
79
|
-
pos = 0
|
|
80
|
-
while pos < len(data):
|
|
81
|
-
tag, pos = _read_varint(data, pos)
|
|
82
|
-
field_num = tag >> 3
|
|
83
|
-
wire_type = tag & 0x7
|
|
84
|
-
if field_num == 1 and wire_type == 5: # min (float)
|
|
85
|
-
obj.min, pos = _read_float32(data, pos)
|
|
86
|
-
elif field_num == 2 and wire_type == 5: # max (float)
|
|
87
|
-
obj.max, pos = _read_float32(data, pos)
|
|
88
|
-
elif field_num == 3 and wire_type == 0: # bits (uint32)
|
|
89
|
-
obj.bits, pos = _read_varint(data, pos)
|
|
90
|
-
else:
|
|
91
|
-
pos = _skip_field(data, pos, wire_type)
|
|
92
|
-
return obj
|
|
93
|
-
|
|
94
|
-
def __repr__(self):
|
|
95
|
-
return f'QuantizedFloatOptions(min={self.min}, max={self.max}, bits={self.bits})'
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
class BitPackedOptions:
|
|
99
|
-
"""Mirrors the BitPackedOptions proto message."""
|
|
100
|
-
|
|
101
|
-
__slots__ = ('bits',)
|
|
102
|
-
|
|
103
|
-
def __init__(self, bits: int = 0):
|
|
104
|
-
self.bits = bits
|
|
105
|
-
|
|
106
|
-
@classmethod
|
|
107
|
-
def from_bytes(cls, data: bytes) -> 'BitPackedOptions':
|
|
108
|
-
obj = cls()
|
|
109
|
-
pos = 0
|
|
110
|
-
while pos < len(data):
|
|
111
|
-
tag, pos = _read_varint(data, pos)
|
|
112
|
-
field_num = tag >> 3
|
|
113
|
-
wire_type = tag & 0x7
|
|
114
|
-
if field_num == 1 and wire_type == 0: # bits (uint32)
|
|
115
|
-
obj.bits, pos = _read_varint(data, pos)
|
|
116
|
-
else:
|
|
117
|
-
pos = _skip_field(data, pos, wire_type)
|
|
118
|
-
return obj
|
|
119
|
-
|
|
120
|
-
def __repr__(self):
|
|
121
|
-
return f'BitPackedOptions(bits={self.bits})'
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
# ---------------------------------------------------------------------------
|
|
125
|
-
# Public API
|
|
126
|
-
# ---------------------------------------------------------------------------
|
|
127
|
-
|
|
128
|
-
def get_field_options(field_options_proto):
|
|
129
|
-
"""
|
|
130
|
-
Extract custom bitwise options from a FieldDescriptorProto.options object.
|
|
131
|
-
|
|
132
|
-
Serialises the options message to bytes and walks the wire-format stream
|
|
133
|
-
looking for extension fields 50001 (quantized) and 50002 (bit_packed).
|
|
134
|
-
|
|
135
|
-
Args:
|
|
136
|
-
field_options_proto: google.protobuf.descriptor_pb2.FieldOptions instance
|
|
137
|
-
(may be a default/empty instance when no options are set).
|
|
138
|
-
|
|
139
|
-
Returns:
|
|
140
|
-
tuple[QuantizedFloatOptions | None, BitPackedOptions | None]
|
|
141
|
-
"""
|
|
142
|
-
try:
|
|
143
|
-
raw = field_options_proto.SerializeToString()
|
|
144
|
-
except Exception:
|
|
145
|
-
return None, None
|
|
146
|
-
|
|
147
|
-
if not raw:
|
|
148
|
-
return None, None
|
|
149
|
-
|
|
150
|
-
quantized = None
|
|
151
|
-
bit_packed = None
|
|
152
|
-
pos = 0
|
|
153
|
-
|
|
154
|
-
while pos < len(raw):
|
|
155
|
-
tag, pos = _read_varint(raw, pos)
|
|
156
|
-
field_num = tag >> 3
|
|
157
|
-
wire_type = tag & 0x7
|
|
158
|
-
|
|
159
|
-
if wire_type == 2: # length-delimited
|
|
160
|
-
length, pos = _read_varint(raw, pos)
|
|
161
|
-
value_bytes = raw[pos:pos + length]
|
|
162
|
-
pos += length
|
|
163
|
-
if field_num == QUANTIZED_FIELD_NUMBER:
|
|
164
|
-
quantized = QuantizedFloatOptions.from_bytes(value_bytes)
|
|
165
|
-
elif field_num == BIT_PACKED_FIELD_NUMBER:
|
|
166
|
-
bit_packed = BitPackedOptions.from_bytes(value_bytes)
|
|
167
|
-
# else: unknown length-delimited field — already consumed
|
|
168
|
-
else:
|
|
169
|
-
pos = _skip_field(raw, pos, wire_type)
|
|
170
|
-
|
|
171
|
-
return quantized, bit_packed
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
protoc-gen-bitwise — protoc plugin that generates C# bitwise serialization code.
|
|
4
|
-
|
|
5
|
-
Protocol:
|
|
6
|
-
1. protoc writes a serialised CodeGeneratorRequest to this process's stdin.
|
|
7
|
-
2. This plugin reads it, generates C# partial classes with Encode / Decode
|
|
8
|
-
methods for every message that carries [(quantized)] or [(bit_packed)]
|
|
9
|
-
field annotations.
|
|
10
|
-
3. A serialised CodeGeneratorResponse is written to stdout.
|
|
11
|
-
|
|
12
|
-
Usage (from project root):
|
|
13
|
-
protoc \\
|
|
14
|
-
--proto_path=proto \\
|
|
15
|
-
--bitwise_out=generated/ \\
|
|
16
|
-
--plugin=protoc-gen-bitwise \\
|
|
17
|
-
proto/my_messages.proto
|
|
18
|
-
|
|
19
|
-
Windows invocation (plugin not on PATH as executable):
|
|
20
|
-
protoc \\
|
|
21
|
-
--proto_path=proto \\
|
|
22
|
-
--bitwise_out=generated/ \\
|
|
23
|
-
--plugin=protoc-gen-bitwise=python protoc-gen-bitwise/plugin.py \\
|
|
24
|
-
proto/my_messages.proto
|
|
25
|
-
|
|
26
|
-
Dependencies:
|
|
27
|
-
pip install grpcio-tools # or: pip install protobuf
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
import os
|
|
31
|
-
import sys
|
|
32
|
-
|
|
33
|
-
# Ensure sibling modules (generator_csharp, options_pb2) are importable
|
|
34
|
-
# regardless of where protoc invokes this script from.
|
|
35
|
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
36
|
-
|
|
37
|
-
# On Windows, stdin/stdout are opened in text mode by default which corrupts
|
|
38
|
-
# the binary protobuf payload.
|
|
39
|
-
if sys.platform == 'win32':
|
|
40
|
-
import msvcrt
|
|
41
|
-
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
|
|
42
|
-
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
|
43
|
-
|
|
44
|
-
from google.protobuf.compiler import plugin_pb2
|
|
45
|
-
|
|
46
|
-
from generator_csharp import generate_csharp
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def main() -> None:
|
|
50
|
-
request_bytes = sys.stdin.buffer.read()
|
|
51
|
-
|
|
52
|
-
request = plugin_pb2.CodeGeneratorRequest()
|
|
53
|
-
request.ParseFromString(request_bytes)
|
|
54
|
-
|
|
55
|
-
response = plugin_pb2.CodeGeneratorResponse()
|
|
56
|
-
# Advertise proto3-optional support so protoc does not reject the plugin.
|
|
57
|
-
response.supported_features = (
|
|
58
|
-
plugin_pb2.CodeGeneratorResponse.FEATURE_PROTO3_OPTIONAL
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
# Build a lookup map for all file descriptors (needed for imports, though
|
|
62
|
-
# the current generator only uses the directly requested files).
|
|
63
|
-
file_by_name = {f.name: f for f in request.proto_file}
|
|
64
|
-
|
|
65
|
-
for file_name in request.file_to_generate:
|
|
66
|
-
# Skip the options definition file itself — it has no messages to generate.
|
|
67
|
-
if file_name == 'decentraland/common/options.proto':
|
|
68
|
-
continue
|
|
69
|
-
|
|
70
|
-
file_proto = file_by_name.get(file_name)
|
|
71
|
-
if file_proto is None:
|
|
72
|
-
continue
|
|
73
|
-
|
|
74
|
-
try:
|
|
75
|
-
generated = generate_csharp(file_proto)
|
|
76
|
-
except Exception as exc: # noqa: BLE001
|
|
77
|
-
error = response.file.add()
|
|
78
|
-
error.name = '' # empty name signals an error to protoc
|
|
79
|
-
# Append error text; protoc will print it and fail.
|
|
80
|
-
response.error = f'protoc-gen-bitwise: error processing {file_name}: {exc}'
|
|
81
|
-
continue
|
|
82
|
-
|
|
83
|
-
if generated is not None:
|
|
84
|
-
out = response.file.add()
|
|
85
|
-
out.name = generated['name']
|
|
86
|
-
out.content = generated['content']
|
|
87
|
-
|
|
88
|
-
sys.stdout.buffer.write(response.SerializeToString())
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if __name__ == '__main__':
|
|
92
|
-
main()
|