diff options
Diffstat (limited to 'src/ProtoGen')
28 files changed, 2144 insertions, 0 deletions
diff --git a/src/ProtoGen/DependencyResolutionException.cs b/src/ProtoGen/DependencyResolutionException.cs new file mode 100644 index 00000000..4e483fc1 --- /dev/null +++ b/src/ProtoGen/DependencyResolutionException.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Google.ProtocolBuffers.ProtoGen { + /// <summary> + /// Exception thrown when dependencies within a descriptor set can't be resolved. + /// </summary> + public sealed class DependencyResolutionException : Exception { + public DependencyResolutionException(string message) : base(message) { + } + + public DependencyResolutionException(string format, params object[] args) + : base(string.Format(format, args)) { + } + } +} diff --git a/src/ProtoGen/DescriptorUtil.cs b/src/ProtoGen/DescriptorUtil.cs new file mode 100644 index 00000000..e70ad468 --- /dev/null +++ b/src/ProtoGen/DescriptorUtil.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Google.ProtocolBuffers.Descriptors; +using Google.ProtocolBuffers.DescriptorProtos; + +namespace Google.ProtocolBuffers.ProtoGen { + /// <summary> + /// Utility class for determining namespaces etc. + /// </summary> + internal static class DescriptorUtil { + + internal static bool NestClasses(IDescriptor descriptor) { + // Defaults to false + return descriptor.File.Options.GetExtension(CSharpOptions.CSharpNestClasses); + } + + internal static string GetNamespace(FileDescriptor descriptor) { + if (descriptor.Name == "google/protobuf/descriptor.proto") { + return typeof(DescriptorProtoFile).Namespace; + } + return descriptor.Options.HasExtension(CSharpOptions.CSharpNamespace) ? + descriptor.Options.GetExtension(CSharpOptions.CSharpNamespace) : descriptor.Package; + } + + // Groups are hacky: The name of the field is just the lower-cased name + // of the group type. In C#, though, we would like to retain the original + // capitalization of the type name. + internal static string GetFieldName(FieldDescriptor descriptor) { + if (descriptor.FieldType == FieldType.Group) { + return descriptor.MessageType.Name; + } else { + return descriptor.Name; + } + } + + internal static string GetClassName(IDescriptor descriptor) { + return ToCSharpName(descriptor.FullName, descriptor.File); + } + + internal static string GetFullUmbrellaClassName(FileDescriptor descriptor) { + string result = GetNamespace(descriptor); + if (result != "") result += '.'; + result += GetUmbrellaClassName(descriptor); + return "global::" + result; + } + + internal static string GetUmbrellaClassName(FileDescriptor descriptor) { + if (descriptor.Name == "google/protobuf/descriptor.proto") { + return typeof(DescriptorProtoFile).Name; + } + FileOptions options = descriptor.Options; + if (options.HasExtension(CSharpOptions.CSharpUmbrellaClassname)) { + return descriptor.Options.GetExtension(CSharpOptions.CSharpUmbrellaClassname); + } + int lastSlash = descriptor.Name.LastIndexOf('/'); + string baseName = descriptor.Name.Substring(lastSlash + 1); + return Helpers.UnderscoresToPascalCase(StripProto(baseName)); + } + + private static string StripProto(string text) { + if (!Helpers.StripSuffix(ref text, ".protodevel")) { + Helpers.StripSuffix(ref text, ".proto"); + } + return text; + } + + private static string ToCSharpName(string name, FileDescriptor file) { + string result; + if (!NestClasses(file)) { + result = GetNamespace(file); + } else { + result = GetUmbrellaClassName(file); + } + if (result != "") { + result += '.'; + } + string classname; + if (file.Package == "") { + classname = name; + } else { + // Strip the proto package from full_name since we've replaced it with + // the C# namespace. + classname = name.Substring(file.Package.Length + 1); + } + result += classname.Replace(".", ".Types."); + return "global::" + result; + } + + internal static string GetMappedTypeName(MappedType type) { + switch(type) { + case MappedType.Int32: return "int"; + case MappedType.Int64: return "long"; + case MappedType.UInt32: return "uint"; + case MappedType.UInt64: return "ulong"; + case MappedType.Single: return "float"; + case MappedType.Double: return "double"; + case MappedType.Boolean: return "bool"; + case MappedType.String: return "string"; + case MappedType.ByteString: return "pb::ByteString"; + case MappedType.Enum: return null; + case MappedType.Message: return null; + default: + throw new ArgumentOutOfRangeException("Unknown mapped type " + type); + } + } + } +} diff --git a/src/ProtoGen/EnumFieldGenerator.cs b/src/ProtoGen/EnumFieldGenerator.cs new file mode 100644 index 00000000..9f6a56f2 --- /dev/null +++ b/src/ProtoGen/EnumFieldGenerator.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Google.ProtocolBuffers.Descriptors; + +namespace Google.ProtocolBuffers.ProtoGen { + internal class EnumFieldGenerator : FieldGeneratorBase, IFieldSourceGenerator { + internal EnumFieldGenerator(FieldDescriptor descriptor) + : base(descriptor) { + } + + public void GenerateMembers(TextGenerator writer) { + writer.WriteLine("private bool has{0};", CapitalizedName); + writer.WriteLine("private {0} {1}_ = {2};", TypeName, Name, DefaultValue); + writer.WriteLine("public bool Has{0} {{", CapitalizedName); + writer.WriteLine(" get {{ return has{0}; }}", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public {0} {1} {{", TypeName, PropertyName); + writer.WriteLine(" get {{ return {0}_; }}", Name); + writer.WriteLine("}"); + } + + public void GenerateBuilderMembers(TextGenerator writer) { + writer.WriteLine("public bool Has{0} {{", CapitalizedName); + writer.WriteLine(" get {{ return result.Has{0}; }}", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public {0} {1} {{", TypeName, PropertyName); + writer.WriteLine(" get {{ return result.{0}; }}", PropertyName); + writer.WriteLine(" set {{ Set{0}(value); }}", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public Builder Set{0}({1} value) {{", CapitalizedName, TypeName); + writer.WriteLine(" result.has{0} = true;", CapitalizedName); + writer.WriteLine(" result.{0}_ = value;", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder Clear{0}() {{", CapitalizedName); + writer.WriteLine(" result.has{0} = false;", CapitalizedName); + writer.WriteLine(" result.{0}_ = {1};", Name, DefaultValue); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + } + + public void GenerateMergingCode(TextGenerator writer) { + writer.WriteLine("if (other.Has{0}) {{", CapitalizedName); + writer.WriteLine(" {0} = other.{0};", PropertyName); + writer.WriteLine("}"); + } + + public void GenerateBuildingCode(TextGenerator writer) { + // Nothing to do here for enum types + } + + public void GenerateParsingCode(TextGenerator writer) { + // TODO(jonskeet): Make a more efficient way of doing this + writer.WriteLine("int rawValue = input.ReadEnum();"); + writer.WriteLine("if (!global::System.Enum.IsDefined(typeof({0}), rawValue)) {{", TypeName); + writer.WriteLine(" unknownFields.MergeVarintField({0}, (ulong) rawValue);", Number); + writer.WriteLine("} else {"); + writer.WriteLine(" {0} = ({1}) rawValue;", PropertyName, TypeName); + writer.WriteLine("}"); + } + + public void GenerateSerializationCode(TextGenerator writer) { + writer.WriteLine("if (Has{0}) {{", CapitalizedName); + writer.WriteLine(" output.WriteEnum({0}, (int) {1});", Number, PropertyName); + writer.WriteLine("}"); + } + + public void GenerateSerializedSizeCode(TextGenerator writer) { + writer.WriteLine("if (Has{0}) {{", CapitalizedName); + writer.WriteLine(" size += pb::CodedOutputStream.ComputeEnumSize({0}, (int) {1});", Number, PropertyName); + writer.WriteLine("}"); + } + } +} diff --git a/src/ProtoGen/EnumGenerator.cs b/src/ProtoGen/EnumGenerator.cs new file mode 100644 index 00000000..0de6b89c --- /dev/null +++ b/src/ProtoGen/EnumGenerator.cs @@ -0,0 +1,19 @@ +using Google.ProtocolBuffers.Descriptors; + +namespace Google.ProtocolBuffers.ProtoGen { + internal class EnumGenerator : SourceGeneratorBase<EnumDescriptor>, ISourceGenerator { + internal EnumGenerator(EnumDescriptor descriptor) : base(descriptor) { + } + + public void Generate(TextGenerator writer) { + writer.WriteLine("{0} enum {1} {{", ClassAccessLevel, Descriptor.Name); + writer.Indent(); + foreach (EnumValueDescriptor value in Descriptor.Values) { + writer.WriteLine("{0} = {1},", value.Name, value.Number); + } + writer.Outdent(); + writer.WriteLine("}"); + writer.WriteLine(); + } + } +} diff --git a/src/ProtoGen/ExtensionGenerator.cs b/src/ProtoGen/ExtensionGenerator.cs new file mode 100644 index 00000000..9c235456 --- /dev/null +++ b/src/ProtoGen/ExtensionGenerator.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Google.ProtocolBuffers.Descriptors; + +namespace Google.ProtocolBuffers.ProtoGen { + internal class ExtensionGenerator : SourceGeneratorBase<FieldDescriptor>, ISourceGenerator { + internal ExtensionGenerator(FieldDescriptor descriptor) : base(descriptor) { + } + + public void Generate(TextGenerator writer) { + string name = Helpers.UnderscoresToPascalCase(DescriptorUtil.GetFieldName(Descriptor)); + + string type; + switch (Descriptor.MappedType) { + case MappedType.Message: + type = DescriptorUtil.GetClassName(Descriptor.MessageType); + break; + case MappedType.Enum: + type = DescriptorUtil.GetClassName(Descriptor.EnumType); + break; + default: + type = DescriptorUtil.GetMappedTypeName(Descriptor.MappedType); + break; + } + + if (Descriptor.IsRepeated) { + writer.WriteLine("{0} static readonly", ClassAccessLevel); + writer.WriteLine(" pb::GeneratedExtensionBase<scg::IList<{0}>> {1} =", type, name); + writer.WriteLine(" pb::GeneratedRepeatExtension<{0}>.CreateInstance(Descriptor.Extensions[{1}]);", type, Descriptor.Index); + } else { + writer.WriteLine("{0} static readonly pb::GeneratedExtensionBase<{1}> {2} =", ClassAccessLevel, type, name); + writer.WriteLine(" pb::GeneratedSingleExtension<{0}>.CreateInstance(Descriptor.Extensions[{1}]);", type, Descriptor.Index); + } + } + } +} diff --git a/src/ProtoGen/FieldGeneratorBase.cs b/src/ProtoGen/FieldGeneratorBase.cs new file mode 100644 index 00000000..d050ac84 --- /dev/null +++ b/src/ProtoGen/FieldGeneratorBase.cs @@ -0,0 +1,130 @@ +using System; +using Google.ProtocolBuffers.Descriptors; +using System.Globalization; + +namespace Google.ProtocolBuffers.ProtoGen { + internal abstract class FieldGeneratorBase : SourceGeneratorBase<FieldDescriptor> { + protected FieldGeneratorBase(FieldDescriptor descriptor) + : base(descriptor) { + } + + private static bool AllPrintableAscii(string text) { + foreach (char c in text) { + if (c < 0x20 || c > 0x7e) { + return false; + } + } + return true; + } + + protected string DefaultValue { + get { + string suffix = ""; + switch (Descriptor.FieldType) { + case FieldType.Float: suffix = "F"; break; + case FieldType.Double: suffix = "D"; break; + case FieldType.Int64: suffix = "L"; break; + case FieldType.UInt64: suffix = "UL"; break; + } + switch (Descriptor.FieldType) { + case FieldType.Float: + case FieldType.Double: + case FieldType.Int32: + case FieldType.Int64: + case FieldType.SInt32: + case FieldType.SInt64: + case FieldType.SFixed32: + case FieldType.SFixed64: + case FieldType.UInt32: + case FieldType.UInt64: + case FieldType.Fixed32: + case FieldType.Fixed64: + // The simple Object.ToString converts using the current culture. + // We want to always use the invariant culture so it's predictable. + IConvertible value = (IConvertible) Descriptor.DefaultValue; + return value.ToString(CultureInfo.InvariantCulture) + suffix; + case FieldType.Bool: + return (bool) Descriptor.DefaultValue ? "true" : "false"; + + case FieldType.Bytes: + if (!Descriptor.HasDefaultValue) { + return "pb::ByteString.Empty"; + } + return string.Format("(pb::ByteString) {0}.Descriptor.Fields[{1}].DefaultValue", DescriptorUtil.GetClassName(Descriptor.ContainingType), Descriptor.Index); + case FieldType.String: + if (AllPrintableAscii(Descriptor.Proto.DefaultValue)) { + // All chars are ASCII and printable. In this case we only + // need to escape quotes and backslashes. + return "\"" + Descriptor.Proto.DefaultValue + .Replace("\\", "\\\\") + .Replace("'", "\\'") + .Replace("\"", "\\\"") + + "\""; + } + return string.Format("(string) {0}.Descriptor.Fields[{1}].DefaultValue", DescriptorUtil.GetClassName(Descriptor.ContainingType), Descriptor.Index); + case FieldType.Enum: + return TypeName + "." + ((EnumValueDescriptor) Descriptor.DefaultValue).Name; + case FieldType.Message: + case FieldType.Group: + return TypeName + ".DefaultInstance"; + default: + throw new InvalidOperationException("Invalid field descriptor type"); + } + } + } + + /// <summary> + /// Usually the same as CapitalizedName, except when the enclosing type has the same name, + /// in which case an underscore is appended. + /// </summary> + protected string PropertyName { + get { + string ret = CapitalizedName; + if (ret == Descriptor.ContainingType.Name) { + ret += "_"; + } + return ret; + } + } + + protected string CapitalizedName { + get { return Helpers.UnderscoresToPascalCase(DescriptorUtil.GetFieldName(Descriptor)); } + } + + protected string Name { + get { return Helpers.UnderscoresToCamelCase(DescriptorUtil.GetFieldName(Descriptor)); } + } + + protected int Number { + get { return Descriptor.FieldNumber; } + } + + protected string TypeName { + get { + switch (Descriptor.FieldType) { + case FieldType.Enum: + return DescriptorUtil.GetClassName(Descriptor.EnumType); + case FieldType.Message: + case FieldType.Group: + return DescriptorUtil.GetClassName(Descriptor.MessageType); + default: + return DescriptorUtil.GetMappedTypeName(Descriptor.MappedType); + } + } + } + + protected string MessageOrGroup { + get { return Descriptor.FieldType == FieldType.Group ? "Group" : "Message"; } + } + + /// <summary> + /// Returns the type name as used in CodedInputStream method names: SFixed32, UInt32 etc. + /// </summary> + protected string CapitalizedTypeName { + get { + // Our enum names match perfectly. How serendipitous. + return Descriptor.FieldType.ToString(); + } + } + } +} diff --git a/src/ProtoGen/Generator.cs b/src/ProtoGen/Generator.cs new file mode 100644 index 00000000..f656386b --- /dev/null +++ b/src/ProtoGen/Generator.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Google.ProtocolBuffers.DescriptorProtos; +using System.IO; +using Google.ProtocolBuffers.Descriptors; +using Google.ProtocolBuffers.Collections; + +namespace Google.ProtocolBuffers.ProtoGen { + /// <summary> + /// Code generator for protocol buffers. Only C# is supported at the moment. + /// </summary> + public sealed class Generator { + + readonly GeneratorOptions options; + + private Generator(GeneratorOptions options) { + options.Validate(); + this.options = options; + } + + /// <summary> + /// Returns a generator configured with the specified options. + /// </summary> + public static Generator CreateGenerator(GeneratorOptions options) { + return new Generator(options); + } + + public void Generate() { + foreach (string inputFile in options.InputFiles) { + FileDescriptorSet descriptorProtos; + ExtensionRegistry extensionRegistry = ExtensionRegistry.CreateInstance(); + extensionRegistry.Add(CSharpOptions.CSharpUmbrellaClassname); + extensionRegistry.Add(CSharpOptions.CSharpMultipleFiles); + extensionRegistry.Add(CSharpOptions.CSharpNamespace); + extensionRegistry.Add(CSharpOptions.CSharpNestClasses); + extensionRegistry.Add(CSharpOptions.CSharpPublicClasses); + using (Stream inputStream = File.OpenRead(inputFile)) { + descriptorProtos = FileDescriptorSet.ParseFrom(inputStream, extensionRegistry); + } + IList<FileDescriptor> descriptors = ConvertDescriptors(descriptorProtos); + + foreach (FileDescriptor descriptor in descriptors) { + Generate(descriptor); + } + } + } + + /// <summary> + /// Generates code for a particular file. All dependencies must + /// already have been resolved. + /// </summary> + private void Generate(FileDescriptor descriptor) { + string umbrellaClass = DescriptorUtil.GetUmbrellaClassName(descriptor); + string ns = DescriptorUtil.GetNamespace(descriptor); + using (TextWriter textWriter = File.CreateText(Path.Combine(options.OutputDirectory, umbrellaClass + ".cs"))) { + TextGenerator writer = new TextGenerator(textWriter); + + UmbrellaClassGenerator ucg = new UmbrellaClassGenerator(descriptor); + ucg.Generate(writer); + /* + GenerateSiblings(umbrellaSource, descriptor, descriptor.MessageTypes); + GenerateSiblings(umbrellaSource, descriptor, descriptor.EnumTypes); + GenerateSiblings(umbrellaSource, descriptor, descriptor.Services);*/ + } + } + + private static void GenerateSiblings<T>(SourceFileGenerator parentSourceGenerator, FileDescriptor file, IEnumerable<T> siblings) + where T : IDescriptor { + } + + /// <summary> + /// Resolves any dependencies and converts FileDescriptorProtos into FileDescriptors. + /// The list returned is in the same order as the protos are listed in the descriptor set. + /// Note: this method is internal rather than private to allow testing. + /// </summary> + /// <exception cref="DependencyResolutionException">Not all dependencies could be resolved.</exception> + internal static IList<FileDescriptor> ConvertDescriptors(FileDescriptorSet descriptorProtos) { + // Simple strategy: Keep going through the list of protos to convert, only doing ones where + // we've already converted all the dependencies, until we get to a stalemate + IList<FileDescriptorProto> fileList = descriptorProtos.FileList; + FileDescriptor[] converted = new FileDescriptor[fileList.Count]; + + Dictionary<string, FileDescriptor> convertedMap = new Dictionary<string, FileDescriptor>(); + + int totalConverted = 0; + + bool madeProgress = true; + while (madeProgress && totalConverted < converted.Length) { + madeProgress = false; + for (int i = 0; i < converted.Length; i++) { + if (converted[i] != null) { + // Already done this one + continue; + } + FileDescriptorProto candidate = fileList[i]; + FileDescriptor[] dependencies = new FileDescriptor[candidate.DependencyList.Count]; + bool foundAllDependencies = true; + for (int j = 0; j < dependencies.Length; j++) { + if (!convertedMap.TryGetValue(candidate.DependencyList[j], out dependencies[j])) { + foundAllDependencies = false; + break; + } + } + if (!foundAllDependencies) { + continue; + } + madeProgress = true; + totalConverted++; + converted[i] = FileDescriptor.BuildFrom(candidate, dependencies); + convertedMap[candidate.Name] = converted[i]; + } + } + if (!madeProgress) { + StringBuilder remaining = new StringBuilder(); + for (int i = 0; i < converted.Length; i++) { + if (converted[i] == null) { + if (remaining.Length != 0) { + remaining.Append(", "); + } + FileDescriptorProto failure = fileList[i]; + remaining.Append(failure.Name); + remaining.Append(":"); + foreach (string dependency in failure.DependencyList) { + if (!convertedMap.ContainsKey(dependency)) { + remaining.Append(" "); + remaining.Append(dependency); + } + } + remaining.Append(";"); + } + } + throw new DependencyResolutionException("Unable to resolve all dependencies: " + remaining); + } + return Lists.AsReadOnly(converted); + } + } +} diff --git a/src/ProtoGen/GeneratorOptions.cs b/src/ProtoGen/GeneratorOptions.cs new file mode 100644 index 00000000..c37352ed --- /dev/null +++ b/src/ProtoGen/GeneratorOptions.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.IO; + +namespace Google.ProtocolBuffers.ProtoGen { + + /// <summary> + /// All the configuration required for the generator - where to generate + /// output files, the location of input files etc. While this isn't immutable + /// in practice, the contents shouldn't be changed after being passed to + /// the generator. + /// </summary> + public sealed class GeneratorOptions { + + public string OutputDirectory { get; set; } + public IList<string> InputFiles { get; set; } + + /// <summary> + /// Attempts to validate the options, but doesn't throw an exception if they're invalid. + /// Instead, when this method returns false, the output variable will contain a collection + /// of reasons for the validation failure. + /// </summary> + /// <param name="reasons">Variable to receive a list of reasons in case of validation failure.</param> + /// <returns>true if the options are valid; false otherwise</returns> + public bool TryValidate(out IList<string> reasons) { + List<string> tmpReasons = new List<string>(); + + // Output directory validation + if (string.IsNullOrEmpty(OutputDirectory)) { + tmpReasons.Add("No output directory specified"); + } else { + if (!Directory.Exists(OutputDirectory)) { + tmpReasons.Add("Specified output directory (" + OutputDirectory + " doesn't exist."); + } + } + + // Input file validation (just in terms of presence) + if (InputFiles == null || InputFiles.Count == 0) { + tmpReasons.Add("No input files specified"); + } else { + foreach (string input in InputFiles) { + FileInfo fi = new FileInfo(input); + if (!fi.Exists) { + tmpReasons.Add("Input file " + input + " doesn't exist."); + } + } + } + + if (tmpReasons.Count != 0) { + reasons = tmpReasons; + return false; + } + + reasons = null; + return true; + } + + /// <summary> + /// Validates that all the options have been set and are valid, + /// throwing an exception if they haven't. + /// </summary> + /// <exception cref="InvalidOptionsException">The options are invalid.</exception> + public void Validate() { + IList<string> reasons; + if (!TryValidate(out reasons)) { + throw new InvalidOptionsException(reasons); + } + } + } +} diff --git a/src/ProtoGen/Helpers.cs b/src/ProtoGen/Helpers.cs new file mode 100644 index 00000000..79a2912c --- /dev/null +++ b/src/ProtoGen/Helpers.cs @@ -0,0 +1,78 @@ +using System; +using System.Text; +using Google.ProtocolBuffers.DescriptorProtos; +using Google.ProtocolBuffers.Descriptors; +namespace Google.ProtocolBuffers.ProtoGen { + + /// <summary> + /// Helpers to resolve class names etc. + /// </summary> + internal static class Helpers { + internal static string UnderscoresToPascalCase(string input) { + return UnderscoresToPascalOrCamelCase(input, true); + } + + internal static string UnderscoresToCamelCase(string input) { + return UnderscoresToPascalOrCamelCase(input, false); + } + + internal static void WriteNamespaces(TextGenerator writer) { + writer.WriteLine("using pb = global::Google.ProtocolBuffers;"); + writer.WriteLine("using pbc = global::Google.ProtocolBuffers.Collections;"); + writer.WriteLine("using pbd = global::Google.ProtocolBuffers.Descriptors;"); + writer.WriteLine("using scg = global::System.Collections.Generic;"); + } + + /// <summary> + /// Converts a string to Pascal or Camel case. The first letter is capitalized or + /// lower-cased depending on <paramref name="pascal"/> is true. + /// After the first letter, any punctuation is removed but triggers capitalization + /// of the next letter. Digits are preserved but trigger capitalization of the next + /// letter. + /// All capitalisation is done in the invariant culture. + /// </summary> + private static string UnderscoresToPascalOrCamelCase(string input, bool pascal) { + StringBuilder result = new StringBuilder(); + bool capitaliseNext = pascal; + for (int i=0; i < input.Length; i++) { + char c = input[i]; + if ('a' <= c && c <= 'z') { + if (capitaliseNext) { + result.Append(char.ToUpperInvariant(c)); + } else { + result.Append(c); + } + capitaliseNext = false; + } else if ('A' <= c && c <= 'Z') { + if (i == 0 && !pascal) { + // Force first letter to lower-case unless explicitly told to + // capitalize it. + result.Append(char.ToLowerInvariant(c)); + } else { + // Capital letters after the first are left as-is. + result.Append(c); + } + capitaliseNext = false; + } else if ('0' <= c && c <= '9') { + result.Append(c); + capitaliseNext = true; + } else { + capitaliseNext = true; + } + } + return result.ToString(); + } + + /// <summary> + /// Attempts to strip a suffix from a string, returning whether + /// or not the suffix was actually present. + /// </summary> + internal static bool StripSuffix(ref string text, string suffix) { + if (text.EndsWith(suffix)) { + text = text.Substring(0, text.Length - suffix.Length); + return true; + } + return false; + } + } +} diff --git a/src/ProtoGen/IFieldSourceGenerator.cs b/src/ProtoGen/IFieldSourceGenerator.cs new file mode 100644 index 00000000..a64043c8 --- /dev/null +++ b/src/ProtoGen/IFieldSourceGenerator.cs @@ -0,0 +1,11 @@ +namespace Google.ProtocolBuffers.ProtoGen { + internal interface IFieldSourceGenerator { + void GenerateMembers(TextGenerator writer); + void GenerateBuilderMembers(TextGenerator writer); + void GenerateMergingCode(TextGenerator writer); + void GenerateBuildingCode(TextGenerator writer); + void GenerateParsingCode(TextGenerator writer); + void GenerateSerializationCode(TextGenerator writer); + void GenerateSerializedSizeCode(TextGenerator writer); + } +} diff --git a/src/ProtoGen/ISourceGenerator.cs b/src/ProtoGen/ISourceGenerator.cs new file mode 100644 index 00000000..16e7c29a --- /dev/null +++ b/src/ProtoGen/ISourceGenerator.cs @@ -0,0 +1,5 @@ +namespace Google.ProtocolBuffers.ProtoGen { + internal interface ISourceGenerator { + void Generate(TextGenerator writer); + } +} diff --git a/src/ProtoGen/InvalidOptionsException.cs b/src/ProtoGen/InvalidOptionsException.cs new file mode 100644 index 00000000..aee03228 --- /dev/null +++ b/src/ProtoGen/InvalidOptionsException.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Google.ProtocolBuffers.Collections; + +namespace Google.ProtocolBuffers.ProtoGen { + /// <summary> + /// Exception thrown to indicate that the options passed were invalid. + /// </summary> + public sealed class InvalidOptionsException : Exception { + + private readonly IList<string> reasons; + + /// <summary> + /// An immutable list of reasons why the options were invalid. + /// </summary> + public IList<string> Reasons { + get { return reasons; } + } + + public InvalidOptionsException(IList<string> reasons) + : base(BuildMessage(reasons)) { + this.reasons = Lists.AsReadOnly(reasons); + } + + private static string BuildMessage(IEnumerable<string> reasons) { + StringBuilder builder = new StringBuilder("Invalid options:"); + builder.AppendLine(); + foreach (string reason in reasons) { + builder.Append(" "); + builder.AppendLine(reason); + } + return builder.ToString(); + } + } +} diff --git a/src/ProtoGen/MessageFieldGenerator.cs b/src/ProtoGen/MessageFieldGenerator.cs new file mode 100644 index 00000000..c73edfc0 --- /dev/null +++ b/src/ProtoGen/MessageFieldGenerator.cs @@ -0,0 +1,92 @@ +using Google.ProtocolBuffers.Descriptors; + +namespace Google.ProtocolBuffers.ProtoGen { + internal class MessageFieldGenerator : FieldGeneratorBase, IFieldSourceGenerator { + + internal MessageFieldGenerator(FieldDescriptor descriptor) + : base(descriptor) { + } + + public void GenerateMembers(TextGenerator writer) { + writer.WriteLine("private bool has{0};", CapitalizedName); + writer.WriteLine("private {0} {1}_ = {2};", TypeName, Name, DefaultValue); + writer.WriteLine("public bool Has{0} {{", CapitalizedName); + writer.WriteLine(" get {{ return has{0}; }}", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public {0} {1} {{", TypeName, CapitalizedName); + writer.WriteLine(" get {{ return {0}_; }}", Name); + writer.WriteLine("}"); + } + + public void GenerateBuilderMembers(TextGenerator writer) { + writer.WriteLine("public bool Has{0} {{", CapitalizedName); + writer.WriteLine(" get {{ return result.Has{0}; }}", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public {0} {1} {{", TypeName, CapitalizedName); + writer.WriteLine(" get {{ return result.{0}; }}", CapitalizedName); + writer.WriteLine(" set {{ Set{0}(value); }}", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public Builder Set{0}({1} value) {{", CapitalizedName, TypeName); + writer.WriteLine(" result.has{0} = true;", CapitalizedName); + writer.WriteLine(" result.{0}_ = value;", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder Set{0}({1}.Builder builderForValue) {{", CapitalizedName, TypeName); + writer.WriteLine(" result.has{0} = true;", CapitalizedName); + writer.WriteLine(" result.{0}_ = builderForValue.Build();", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder Merge{0}({1} value) {{", CapitalizedName, TypeName); + writer.WriteLine(" if (result.Has{0} &&", CapitalizedName); + writer.WriteLine(" result.{0}_ != {1}) {{", Name, DefaultValue); + writer.WriteLine(" result.{0}_ = {1}.CreateBuilder(result.{0}_).MergeFrom(value).BuildPartial();", Name, TypeName); + writer.WriteLine(" } else {"); + writer.WriteLine(" result.{0}_ = value;", Name); + writer.WriteLine(" }"); + writer.WriteLine(" result.has{0} = true;", CapitalizedName); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder Clear{0}() {{", CapitalizedName); + writer.WriteLine(" result.has{0} = false;", CapitalizedName); + writer.WriteLine(" result.{0}_ = {1};", Name, DefaultValue); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + } + + public void GenerateMergingCode(TextGenerator writer) { + writer.WriteLine("if (other.Has{0}) {{", CapitalizedName); + writer.WriteLine(" Merge{0}(other.{0});", CapitalizedName); + writer.WriteLine("}"); + } + + public void GenerateBuildingCode(TextGenerator writer) { + // Nothing to do for singular fields + } + + public void GenerateParsingCode(TextGenerator writer) { + writer.WriteLine("{0}.Builder subBuilder = {0}.CreateBuilder();", TypeName); + writer.WriteLine("if (Has{0}) {{", CapitalizedName); + writer.WriteLine(" subBuilder.MergeFrom({0});", CapitalizedName); + writer.WriteLine("}"); + if (Descriptor.FieldType == FieldType.Group) { + writer.WriteLine("input.ReadGroup({0}, subBuilder, extensionRegistry);", Number); + } else { + writer.WriteLine("input.ReadMessage(subBuilder, extensionRegistry);"); + } + writer.WriteLine("{0} = subBuilder.BuildPartial();", CapitalizedName); + } + + public void GenerateSerializationCode(TextGenerator writer) { + writer.WriteLine("if (Has{0}) {{", CapitalizedName); + writer.WriteLine(" output.Write{0}({1}, {2});", MessageOrGroup, Number, CapitalizedName); + writer.WriteLine("}"); + } + + public void GenerateSerializedSizeCode(TextGenerator writer) { + writer.WriteLine("if (Has{0}) {{", CapitalizedName); + writer.WriteLine(" size += pb::CodedOutputStream.Compute{0}Size({1}, {2});", + MessageOrGroup, Number, CapitalizedName); + writer.WriteLine("}"); + } + } +} diff --git a/src/ProtoGen/MessageGenerator.cs b/src/ProtoGen/MessageGenerator.cs new file mode 100644 index 00000000..95de1a38 --- /dev/null +++ b/src/ProtoGen/MessageGenerator.cs @@ -0,0 +1,453 @@ +using System.Collections; +using Google.ProtocolBuffers.Descriptors; +using Google.ProtocolBuffers.DescriptorProtos; +using System.Collections.Generic; + +using ExtensionRange = Google.ProtocolBuffers.DescriptorProtos.DescriptorProto.Types.ExtensionRange; + +namespace Google.ProtocolBuffers.ProtoGen { + internal class MessageGenerator : SourceGeneratorBase<MessageDescriptor>, ISourceGenerator { + internal MessageGenerator(MessageDescriptor descriptor) : base(descriptor) { + } + + private string ClassName { + get { return Descriptor.Name; } + } + + private string FullClassName { + get { return DescriptorUtil.GetClassName(Descriptor); } + } + + /// <summary> + /// Get an identifier that uniquely identifies this type within the file. + /// This is used to declare static variables related to this type at the + /// outermost file scope. + /// </summary> + static string GetUniqueFileScopeIdentifier(IDescriptor descriptor) { + return "static_" + descriptor.FullName.Replace(".", "_"); + } + + internal void GenerateStaticVariables(TextGenerator writer) { + // Because descriptor.proto (Google.ProtocolBuffers.DescriptorProtos) is + // used in the construction of descriptors, we have a tricky bootstrapping + // problem. To help control static initialization order, we make sure all + // descriptors and other static data that depends on them are members of + // the proto-descriptor class. This way, they will be initialized in + // a deterministic order. + + string identifier = GetUniqueFileScopeIdentifier(Descriptor); + + // The descriptor for this type. + string access = Descriptor.File.Options.GetExtension(CSharpOptions.CSharpNestClasses) ? "private" : "internal"; + writer.WriteLine("{0} static readonly pbd::MessageDescriptor internal__{1}__Descriptor", access, identifier); + if (Descriptor.ContainingType == null) { + writer.WriteLine(" = Descriptor.MessageTypes[{0}];", Descriptor.Index); + } else { + writer.WriteLine(" = internal__{0}__Descriptor.NestedTypes[{1}];", GetUniqueFileScopeIdentifier(Descriptor.ContainingType), Descriptor.Index); + } + writer.WriteLine("{0} static pb::FieldAccess.FieldAccessorTable<{1}, {1}.Builder> internal__{2}__FieldAccessorTable", + access, FullClassName, identifier); + writer.WriteLine(" = new pb::FieldAccess.FieldAccessorTable<{0}, {0}.Builder>(internal__{1}__Descriptor,", + FullClassName, identifier); + writer.Print(" new string[] { "); + foreach (FieldDescriptor field in Descriptor.Fields) { + writer.Write("\"{0}\", ", Helpers.UnderscoresToPascalCase(DescriptorUtil.GetFieldName(field))); + } + writer.WriteLine("});"); + + // Generate static members for all nested types. + foreach (MessageDescriptor nestedMessage in Descriptor.NestedTypes) { + new MessageGenerator(nestedMessage).GenerateStaticVariables(writer); + } + } + + public void Generate(TextGenerator writer) { + writer.WriteLine("{0} sealed partial class {1} : pb::{2}Message<{1}, {1}.Builder> {{", + ClassAccessLevel, ClassName, Descriptor.Proto.ExtensionRangeCount > 0 ? "Extendable" : "Generated"); + writer.Indent(); + // Must call BuildPartial() to make sure all lists are made read-only + writer.WriteLine("private static readonly {0} defaultInstance = new Builder().BuildPartial();", ClassName); + writer.WriteLine("public static {0} DefaultInstance {{", ClassName); + writer.WriteLine(" get { return defaultInstance; }"); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("public override {0} DefaultInstanceForType {{", ClassName); + writer.WriteLine(" get { return defaultInstance; }"); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("protected override {0} ThisMessage {{", ClassName); + writer.WriteLine(" get { return this; }"); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("public static pbd::MessageDescriptor Descriptor {"); + writer.WriteLine(" get {{ return {0}.internal__{1}__Descriptor; }}", DescriptorUtil.GetFullUmbrellaClassName(Descriptor.File), + GetUniqueFileScopeIdentifier(Descriptor)); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("protected override pb::FieldAccess.FieldAccessorTable<{0}, {0}.Builder> InternalFieldAccessors {{", ClassName); + writer.WriteLine(" get {{ return {0}.internal__{1}__FieldAccessorTable; }}", DescriptorUtil.GetFullUmbrellaClassName(Descriptor.File), + GetUniqueFileScopeIdentifier(Descriptor)); + writer.WriteLine("}"); + writer.WriteLine(); + + // Extensions don't need to go in an extra nested type + WriteChildren(writer, null, Descriptor.Extensions); + + if (Descriptor.EnumTypes.Count + Descriptor.NestedTypes.Count > 0) { + writer.WriteLine("#region Nested types"); + writer.WriteLine("public static class Types {"); + writer.Indent(); + WriteChildren(writer, null, Descriptor.EnumTypes); + WriteChildren(writer, null, Descriptor.NestedTypes); + writer.Outdent(); + writer.WriteLine("}"); + writer.WriteLine("#endregion"); + writer.WriteLine(); + } + + foreach(FieldDescriptor fieldDescriptor in Descriptor.Fields) { + // Rats: we lose the debug comment here :( + SourceGenerators.CreateFieldGenerator(fieldDescriptor).GenerateMembers(writer); + writer.WriteLine(); + } + + if (Descriptor.File.Options.OptimizeFor == FileOptions.Types.OptimizeMode.SPEED) { + GenerateIsInitialized(writer); + GenerateMessageSerializationMethods(writer); + } + + GenerateParseFromMethods(writer); + GenerateBuilder(writer); + } + + private void GenerateMessageSerializationMethods(TextGenerator writer) { + List<FieldDescriptor> sortedFields = new List<FieldDescriptor>(Descriptor.Fields); + sortedFields.Sort((f1, f2) => f1.FieldNumber.CompareTo(f2.FieldNumber)); + + List<ExtensionRange> sortedExtensions = new List<ExtensionRange>(Descriptor.Proto.ExtensionRangeList); + sortedExtensions.Sort((r1, r2) => (r1.Start.CompareTo(r2.Start))); + + writer.WriteLine("public override void WriteTo(pb::CodedOutputStream output) {"); + writer.Indent(); + if (Descriptor.Proto.ExtensionRangeList.Count > 0) { + writer.WriteLine("pb::ExtendableMessage<{0}, {0}.Builder>.ExtensionWriter extensionWriter = CreateExtensionWriter(this);", + ClassName); + } + + // Merge the fields and the extension ranges, both sorted by field number. + for (int i = 0, j = 0; i < Descriptor.Fields.Count || j < sortedExtensions.Count; ) { + if (i == Descriptor.Fields.Count) { + GenerateSerializeOneExtensionRange(writer, sortedExtensions[j++]); + } else if (j == sortedExtensions.Count) { + GenerateSerializeOneField(writer, sortedFields[i++]); + } else if (sortedFields[i].FieldNumber < sortedExtensions[j].Start) { + GenerateSerializeOneField(writer, sortedFields[i++]); + } else { + GenerateSerializeOneExtensionRange(writer, sortedExtensions[j++]); + } + } + + if (Descriptor.Proto.Options.MessageSetWireFormat) { + writer.WriteLine("UnknownFields.WriteAsMessageSetTo(output);"); + } else { + writer.WriteLine("UnknownFields.WriteTo(output);"); + } + + writer.Outdent(); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("private int memoizedSerializedSize = -1;"); + writer.WriteLine("public override int SerializedSize {"); + writer.Indent(); + writer.WriteLine("get {"); + writer.Indent(); + writer.WriteLine("int size = memoizedSerializedSize;"); + writer.WriteLine("if (size != -1) return size;"); + writer.WriteLine(); + writer.WriteLine("size = 0;"); + foreach (FieldDescriptor field in Descriptor.Fields) { + SourceGenerators.CreateFieldGenerator(field).GenerateSerializedSizeCode(writer); + } + if (Descriptor.Proto.ExtensionRangeCount > 0) { + writer.WriteLine("size += ExtensionsSerializedSize;"); + } + + if (Descriptor.Options.MessageSetWireFormat) { + writer.WriteLine("size += UnknownFields.SerializedSizeAsMessageSet;"); + } else { + writer.WriteLine("size += UnknownFields.SerializedSize;"); + } + writer.WriteLine("memoizedSerializedSize = size;"); + writer.WriteLine("return size;"); + writer.Outdent(); + writer.WriteLine("}"); + writer.Outdent(); + writer.WriteLine("}"); + writer.WriteLine(); + } + + private static void GenerateSerializeOneField(TextGenerator writer, FieldDescriptor fieldDescriptor) { + SourceGenerators.CreateFieldGenerator(fieldDescriptor).GenerateSerializationCode(writer); + } + + private static void GenerateSerializeOneExtensionRange(TextGenerator writer, ExtensionRange extensionRange) { + writer.WriteLine("extensionWriter.WriteUntil({0}, output);", extensionRange.End); + } + + private void GenerateParseFromMethods(TextGenerator writer) { + // Note: These are separate from GenerateMessageSerializationMethods() + // because they need to be generated even for messages that are optimized + // for code size. + + writer.WriteLine("public static {0} ParseFrom(pb::ByteString data) {{", ClassName); + writer.WriteLine(" return ((Builder) CreateBuilder().MergeFrom(data)).BuildParsed();"); + writer.WriteLine("}"); + writer.WriteLine("public static {0} ParseFrom(pb::ByteString data, pb::ExtensionRegistry extensionRegistry) {{", ClassName); + writer.WriteLine(" return ((Builder) CreateBuilder().MergeFrom(data, extensionRegistry)).BuildParsed();"); + writer.WriteLine("}"); + writer.WriteLine("public static {0} ParseFrom(byte[] data) {{", ClassName); + writer.WriteLine(" return ((Builder) CreateBuilder().MergeFrom(data)).BuildParsed();"); + writer.WriteLine("}"); + writer.WriteLine("public static {0} ParseFrom(byte[] data, pb::ExtensionRegistry extensionRegistry) {{", ClassName); + writer.WriteLine(" return ((Builder) CreateBuilder().MergeFrom(data, extensionRegistry)).BuildParsed();"); + writer.WriteLine("}"); + writer.WriteLine("public static {0} ParseFrom(global::System.IO.Stream input) {{", ClassName); + writer.WriteLine(" return ((Builder) CreateBuilder().MergeFrom(input)).BuildParsed();"); + writer.WriteLine("}"); + writer.WriteLine("public static {0} ParseFrom(global::System.IO.Stream input, pb::ExtensionRegistry extensionRegistry) {{", ClassName); + writer.WriteLine(" return ((Builder) CreateBuilder().MergeFrom(input, extensionRegistry)).BuildParsed();"); + writer.WriteLine("}"); + writer.WriteLine("public static {0} ParseFrom(pb::CodedInputStream input) {{", ClassName); + writer.WriteLine(" return ((Builder) CreateBuilder().MergeFrom(input)).BuildParsed();"); + writer.WriteLine("}"); + writer.WriteLine("public static {0} ParseFrom(pb::CodedInputStream input, pb::ExtensionRegistry extensionRegistry) {{", ClassName); + writer.WriteLine(" return ((Builder) CreateBuilder().MergeFrom(input, extensionRegistry)).BuildParsed();"); + writer.WriteLine("}"); + } + + /// <summary> + /// Returns whether or not the specified message type has any required fields. + /// If it doesn't, calls to check for initialization can be optimised. + /// TODO(jonskeet): Move this into MessageDescriptor? + /// </summary> + private static bool HasRequiredFields(MessageDescriptor descriptor, Dictionary<MessageDescriptor,object> alreadySeen) { + if (alreadySeen.ContainsKey(descriptor)) { + // The type is already in cache. This means that either: + // a. The type has no required fields. + // b. We are in the midst of checking if the type has required fields, + // somewhere up the stack. In this case, we know that if the type + // has any required fields, they'll be found when we return to it, + // and the whole call to HasRequiredFields() will return true. + // Therefore, we don't have to check if this type has required fields + // here. + return false; + } + alreadySeen[descriptor] = descriptor; // Value is irrelevant + + // If the type has extensions, an extension with message type could contain + // required fields, so we have to be conservative and assume such an + // extension exists. + if (descriptor.Extensions.Count > 0) { + return true; + } + + foreach (FieldDescriptor field in descriptor.Fields) { + if (field.IsRequired) { + return true; + } + // Message or group + if (field.MappedType == MappedType.Message) { + if (HasRequiredFields(field.MessageType, alreadySeen)) { + return true; + } + } + } + return false; + } + + private void GenerateBuilder(TextGenerator writer) { + writer.WriteLine("public static Builder CreateBuilder() { return new Builder(); }"); + writer.WriteLine("public override Builder CreateBuilderForType() { return new Builder(); }"); + writer.WriteLine("public static Builder CreateBuilder({0} prototype) {{", ClassName); + writer.WriteLine(" return (Builder) new Builder().MergeFrom(prototype);"); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("{0} sealed partial class Builder : pb::{2}Builder<{1}, Builder> {{", + ClassAccessLevel, ClassName, Descriptor.Proto.ExtensionRangeCount > 0 ? "Extendable" : "Generated"); + writer.Indent(); + writer.WriteLine("protected override Builder ThisBuilder {"); + writer.WriteLine(" get { return this; }"); + writer.WriteLine("}"); + GenerateCommonBuilderMethods(writer); + if (Descriptor.File.Options.OptimizeFor == FileOptions.Types.OptimizeMode.SPEED) { + GenerateBuilderParsingMethods(writer); + } + foreach (FieldDescriptor field in Descriptor.Fields) { + writer.WriteLine(); + // No field comment :( + SourceGenerators.CreateFieldGenerator(field).GenerateBuilderMembers(writer); + } + writer.Outdent(); + writer.WriteLine("}"); + writer.Outdent(); + writer.WriteLine("}"); + writer.WriteLine(); + } + + private void GenerateCommonBuilderMethods(TextGenerator writer) { + writer.WriteLine("{0} Builder() {{}}", ClassAccessLevel); + writer.WriteLine(); + writer.WriteLine("{0} result = new {0}();", ClassName); + writer.WriteLine(); + writer.WriteLine("protected override {0} MessageBeingBuilt {{", ClassName); + writer.WriteLine(" get { return result; }"); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("public override Builder Clear() {"); + writer.WriteLine(" result = new {0}();", ClassName); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("public override Builder Clone() {"); + writer.WriteLine(" return new Builder().MergeFrom(result);"); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("public override pbd::MessageDescriptor DescriptorForType {"); + writer.WriteLine(" get {{ return {0}.Descriptor; }}", ClassName); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("public override {0} DefaultInstanceForType {{", ClassName); + writer.WriteLine(" get {{ return {0}.DefaultInstance; }}", ClassName); + writer.WriteLine("}"); + writer.WriteLine(); + + writer.WriteLine("public override {0} BuildPartial() {{", ClassName); + writer.Indent(); + foreach (FieldDescriptor field in Descriptor.Fields) { + SourceGenerators.CreateFieldGenerator(field).GenerateBuildingCode(writer); + } + writer.WriteLine("{0} returnMe = result;", ClassName); + writer.WriteLine("result = null;"); + writer.WriteLine("return returnMe;"); + writer.Outdent(); + writer.WriteLine("}"); + writer.WriteLine(); + + if (Descriptor.File.Options.OptimizeFor == FileOptions.Types.OptimizeMode.SPEED) { + writer.WriteLine("public override Builder MergeFrom(pb::IMessage other) {"); + writer.WriteLine(" if (other is {0}) {{", ClassName); + writer.WriteLine(" return MergeFrom(({0}) other);", ClassName); + writer.WriteLine(" } else {"); + writer.WriteLine(" base.MergeFrom(other);"); + writer.WriteLine(" return this;"); + writer.WriteLine(" }"); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("public override Builder MergeFrom({0} other) {{", ClassName); + // Optimization: If other is the default instance, we know none of its + // fields are set so we can skip the merge. + writer.Indent(); + writer.WriteLine("if (other == {0}.DefaultInstance) return this;", ClassName); + foreach (FieldDescriptor field in Descriptor.Fields) { + SourceGenerators.CreateFieldGenerator(field).GenerateMergingCode(writer); + } + writer.WriteLine("this.MergeUnknownFields(other.UnknownFields);"); + writer.WriteLine("return this;"); + writer.Outdent(); + writer.WriteLine("}"); + writer.WriteLine(); + } + } + + private void GenerateBuilderParsingMethods(TextGenerator writer) { + List<FieldDescriptor> sortedFields = new List<FieldDescriptor>(Descriptor.Fields); + sortedFields.Sort((f1, f2) => f1.FieldNumber.CompareTo(f2.FieldNumber)); + + writer.WriteLine("public override Builder MergeFrom(pb::CodedInputStream input) {"); + writer.WriteLine(" return MergeFrom(input, pb::ExtensionRegistry.Empty);"); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("public override Builder MergeFrom(pb::CodedInputStream input, pb::ExtensionRegistry extensionRegistry) {"); + writer.Indent(); + writer.WriteLine("pb::UnknownFieldSet.Builder unknownFields = pb::UnknownFieldSet.CreateBuilder(this.UnknownFields);"); + writer.WriteLine("while (true) {"); + writer.Indent(); + writer.WriteLine("uint tag = input.ReadTag();"); + writer.WriteLine("switch (tag) {"); + writer.Indent(); + writer.WriteLine("case 0: {"); // 0 signals EOF / limit reached + writer.WriteLine(" this.UnknownFields = unknownFields.Build();"); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("default: {"); + writer.WriteLine(" if (!ParseUnknownField(input, unknownFields, extensionRegistry, tag)) {"); + writer.WriteLine(" this.UnknownFields = unknownFields.Build();"); + writer.WriteLine(" return this;"); // it's an endgroup tag + writer.WriteLine(" }"); + writer.WriteLine(" break;"); + writer.WriteLine("}"); + foreach (FieldDescriptor field in sortedFields) { + uint tag = WireFormat.MakeTag(field.FieldNumber, WireFormat.GetWireType(field.FieldType)); + writer.WriteLine("case {0}: {{", tag); + writer.Indent(); + SourceGenerators.CreateFieldGenerator(field).GenerateParsingCode(writer); + writer.WriteLine("break;"); + writer.Outdent(); + writer.WriteLine("}"); + } + writer.Outdent(); + writer.WriteLine("}"); + writer.Outdent(); + writer.WriteLine("}"); + writer.Outdent(); + writer.WriteLine("}"); + writer.WriteLine(); + } + + private void GenerateIsInitialized(TextGenerator writer) { + writer.WriteLine("public override bool IsInitialized {"); + writer.Indent(); + writer.WriteLine("get {"); + writer.Indent(); + + // Check that all required fields in this message are set. + // TODO(kenton): We can optimize this when we switch to putting all the + // "has" fields into a single bitfield. + foreach (FieldDescriptor field in Descriptor.Fields) { + if (field.IsRequired) { + writer.WriteLine("if (!has{0}) return false;", Helpers.UnderscoresToPascalCase(field.Name)); + } + } + + // Now check that all embedded messages are initialized. + foreach (FieldDescriptor field in Descriptor.Fields) { + if (field.FieldType != FieldType.Message || + !HasRequiredFields(field.MessageType, new Dictionary<MessageDescriptor, object>())) { + continue; + } + string propertyName = Helpers.UnderscoresToPascalCase(DescriptorUtil.GetFieldName(field)); + if (field.IsRepeated) { + writer.WriteLine("foreach ({0} element in {1}List) {{", DescriptorUtil.GetClassName(field.MessageType), propertyName); + writer.WriteLine(" if (!element.IsInitialized) return false;"); + writer.WriteLine("}"); + } else if (field.IsOptional) { + writer.WriteLine("if (Has{0}) {{", propertyName); + writer.WriteLine(" if (!{0}.IsInitialized) return false;", propertyName); + writer.WriteLine("}"); + } else { + writer.WriteLine("if (!{0}.IsInitialized) return false;", propertyName); + } + } + + if (Descriptor.Proto.ExtensionRangeCount > 0) { + writer.WriteLine("if (!ExtensionsAreInitialized) return false;"); + } + writer.WriteLine("return true;"); + writer.Outdent(); + writer.WriteLine("}"); + writer.Outdent(); + writer.WriteLine("}"); + writer.WriteLine(); + } + } +} diff --git a/src/ProtoGen/PrimitiveFieldGenerator.cs b/src/ProtoGen/PrimitiveFieldGenerator.cs new file mode 100644 index 00000000..9107def9 --- /dev/null +++ b/src/ProtoGen/PrimitiveFieldGenerator.cs @@ -0,0 +1,70 @@ +using System; +using Google.ProtocolBuffers.Descriptors; + +namespace Google.ProtocolBuffers.ProtoGen { + // TODO(jonskeet): Refactor this. There's loads of common code here. + internal class PrimitiveFieldGenerator : FieldGeneratorBase, IFieldSourceGenerator { + + internal PrimitiveFieldGenerator(FieldDescriptor descriptor) + : base(descriptor) { + } + + public void GenerateMembers(TextGenerator writer) { + writer.WriteLine("private bool has{0};", CapitalizedName); + writer.WriteLine("private {0} {1}_ = {2};", TypeName, Name, DefaultValue); + writer.WriteLine("public bool Has{0} {{", CapitalizedName); + writer.WriteLine(" get {{ return has{0}; }}", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public {0} {1} {{", TypeName, PropertyName); + writer.WriteLine(" get {{ return {0}_; }}", Name); + writer.WriteLine("}"); + } + + public void GenerateBuilderMembers(TextGenerator writer) { + writer.WriteLine("public bool Has{0} {{", CapitalizedName); + writer.WriteLine(" get {{ return result.Has{0}; }}", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public {0} {1} {{", TypeName, PropertyName); + writer.WriteLine(" get {{ return result.{0}; }}", PropertyName); + writer.WriteLine(" set {{ Set{0}(value); }}", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public Builder Set{0}({1} value) {{", CapitalizedName, TypeName); + writer.WriteLine(" result.has{0} = true;", CapitalizedName); + writer.WriteLine(" result.{0}_ = value;", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder Clear{0}() {{", CapitalizedName); + writer.WriteLine(" result.has{0} = false;", CapitalizedName); + writer.WriteLine(" result.{0}_ = {1};", Name, DefaultValue); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + } + + public void GenerateMergingCode(TextGenerator writer) { + writer.WriteLine("if (other.Has{0}) {{", CapitalizedName); + writer.WriteLine(" {0} = other.{0};", PropertyName); + writer.WriteLine("}"); + } + + public void GenerateBuildingCode(TextGenerator writer) { + // Nothing to do here for primitive types + } + + public void GenerateParsingCode(TextGenerator writer) { + writer.WriteLine("{0} = input.Read{1}();", PropertyName, CapitalizedTypeName); + } + + public void GenerateSerializationCode(TextGenerator writer) { + writer.WriteLine("if (Has{0}) {{", CapitalizedName); + writer.WriteLine(" output.Write{0}({1}, {2});", CapitalizedTypeName, Number, PropertyName); + writer.WriteLine("}"); + } + + public void GenerateSerializedSizeCode(TextGenerator writer) { + writer.WriteLine("if (Has{0}) {{", CapitalizedName); + writer.WriteLine(" size += pb::CodedOutputStream.Compute{0}Size({1}, {2});", + CapitalizedTypeName, Number, PropertyName); + writer.WriteLine("}"); + } + } +} diff --git a/src/ProtoGen/Program.cs b/src/ProtoGen/Program.cs new file mode 100644 index 00000000..08cad358 --- /dev/null +++ b/src/ProtoGen/Program.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using Google.ProtocolBuffers.DescriptorProtos; + +namespace Google.ProtocolBuffers.ProtoGen { + /// <summary> + /// Entry point for the Protocol Buffers generator. + /// </summary> + class Program { + static int Main(string[] args) { + try { + // Hack to make sure everything's initialized + DescriptorProtoFile.Descriptor.ToString(); + GeneratorOptions options = ParseCommandLineArguments(args); + + IList<string> validationFailures; + if (!options.TryValidate(out validationFailures)) { + // We've already got the message-building logic in the exception... + InvalidOptionsException exception = new InvalidOptionsException(validationFailures); + Console.WriteLine(exception.Message); + return 1; + } + + Generator generator = Generator.CreateGenerator(options); + generator.Generate(); + + + return 0; + } catch (Exception e) { + Console.Error.WriteLine("Error: {0}", e.Message); + Console.Error.WriteLine(); + Console.Error.WriteLine("Detailed exception information: {0}", e); + return 1; + } + } + + private static GeneratorOptions ParseCommandLineArguments(string[] args) { + GeneratorOptions options = new GeneratorOptions(); + //string baseDir = "c:\\Users\\Jon\\Documents\\Visual Studio 2008\\Projects\\ProtocolBuffers"; + //options.OutputDirectory = baseDir + "\\tmp"; + //options.InputFiles = new[] { baseDir + "\\protos\\nwind-solo.protobin" }; + options.OutputDirectory = "."; + options.InputFiles = args; + return options; + } + } +}
\ No newline at end of file diff --git a/src/ProtoGen/Properties/AssemblyInfo.cs b/src/ProtoGen/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..24f5d781 --- /dev/null +++ b/src/ProtoGen/Properties/AssemblyInfo.cs @@ -0,0 +1,42 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ProtoGen")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ProtoGen")] +[assembly: AssemblyCopyright("Copyright © 2008")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("7101763b-7a38-41be-87f5-7ede4c554509")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: InternalsVisibleTo("Google.ProtocolBuffers.ProtoGen.Test,PublicKey=" + +"0024000004800000940000000602000000240000525341310004000001000100cf43741ffc3e65" + +"c85707245e144e90f1bb82f20d1b1555846008d4d5d5c9270a980350dcb1ddd40fcdde13c2780c" + +"75c9057123daa5613cb6551e2b8bd2254e8f84b3893369869e5119b752442aef7156c4defc489b" + +"96c44ff801fe8d94199e048f8ff414813c9c811a029bcd697040700dc66982539e9b368cb5e725" + +"feed60f2")] diff --git a/src/ProtoGen/Properties/Google.ProtocolBuffers.ProtoGen.snk b/src/ProtoGen/Properties/Google.ProtocolBuffers.ProtoGen.snk Binary files differnew file mode 100644 index 00000000..8e4d98a5 --- /dev/null +++ b/src/ProtoGen/Properties/Google.ProtocolBuffers.ProtoGen.snk diff --git a/src/ProtoGen/ProtoGen.csproj b/src/ProtoGen/ProtoGen.csproj new file mode 100644 index 00000000..f48df42c --- /dev/null +++ b/src/ProtoGen/ProtoGen.csproj @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProductVersion>9.0.30729</ProductVersion> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{250ADE34-82FD-4BAE-86D5-985FBE589C4A}</ProjectGuid> + <OutputType>Exe</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>Google.ProtocolBuffers.ProtoGen</RootNamespace> + <AssemblyName>ProtoGen</AssemblyName> + <TargetFrameworkVersion>v2.0</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + <TargetFrameworkSubset> + </TargetFrameworkSubset> + <SignAssembly>true</SignAssembly> + <AssemblyOriginatorKeyFile>Properties\Google.ProtocolBuffers.ProtoGen.snk</AssemblyOriginatorKeyFile> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>bin\Debug\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <ItemGroup> + <Reference Include="System" /> + </ItemGroup> + <ItemGroup> + <Compile Include="DescriptorUtil.cs" /> + <Compile Include="EnumFieldGenerator.cs" /> + <Compile Include="EnumGenerator.cs" /> + <Compile Include="ExtensionGenerator.cs" /> + <Compile Include="FieldGeneratorBase.cs" /> + <Compile Include="IFieldSourceGenerator.cs" /> + <Compile Include="ISourceGenerator.cs" /> + <Compile Include="MessageFieldGenerator.cs" /> + <Compile Include="MessageGenerator.cs" /> + <Compile Include="PrimitiveFieldGenerator.cs" /> + <Compile Include="RepeatedEnumFieldGenerator.cs" /> + <Compile Include="RepeatedMessageFieldGenerator.cs" /> + <Compile Include="RepeatedPrimitiveFieldGenerator.cs" /> + <Compile Include="ServiceGenerator.cs" /> + <Compile Include="SourceFileGenerator.cs" /> + <Compile Include="DependencyResolutionException.cs" /> + <Compile Include="Generator.cs" /> + <Compile Include="GeneratorOptions.cs" /> + <Compile Include="Helpers.cs" /> + <Compile Include="InvalidOptionsException.cs" /> + <Compile Include="Program.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="SourceGeneratorBase.cs" /> + <Compile Include="SourceGenerators.cs" /> + <Compile Include="UmbrellaClassGenerator.cs" /> + </ItemGroup> + <ItemGroup> + <None Include="app.config" /> + <None Include="Properties\Google.ProtocolBuffers.ProtoGen.snk" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\ProtocolBuffers\ProtocolBuffers.csproj"> + <Project>{6908BDCE-D925-43F3-94AC-A531E6DF2591}</Project> + <Name>ProtocolBuffers</Name> + </ProjectReference> + </ItemGroup> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <!-- To modify your build process, add your task inside one of the targets below and uncomment it. + Other similar extension points exist, see Microsoft.Common.targets. + <Target Name="BeforeBuild"> + </Target> + <Target Name="AfterBuild"> + </Target> + --> +</Project>
\ No newline at end of file diff --git a/src/ProtoGen/RepeatedEnumFieldGenerator.cs b/src/ProtoGen/RepeatedEnumFieldGenerator.cs new file mode 100644 index 00000000..3a988fbc --- /dev/null +++ b/src/ProtoGen/RepeatedEnumFieldGenerator.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Google.ProtocolBuffers.Descriptors; + +namespace Google.ProtocolBuffers.ProtoGen { + internal class RepeatedEnumFieldGenerator : FieldGeneratorBase, IFieldSourceGenerator { + + internal RepeatedEnumFieldGenerator(FieldDescriptor descriptor) + : base(descriptor) { + } + + public void GenerateMembers(TextGenerator writer) { + writer.WriteLine("private pbc::PopsicleList<{0}> {1}_ = new pbc::PopsicleList<{0}>();", TypeName, Name); + writer.WriteLine("public scg::IList<{0}> {1}List {{", TypeName, CapitalizedName); + writer.WriteLine(" get {{ return pbc::Lists.AsReadOnly({0}_); }}", Name); + writer.WriteLine("}"); + + // TODO(jonskeet): Redundant API calls? Possibly - include for portability though. Maybe create an option. + writer.WriteLine("public int {0}Count {{", CapitalizedName); + writer.WriteLine(" get {{ return {0}_.Count; }}", Name); + writer.WriteLine("}"); + + writer.WriteLine("public {0} Get{1}(int index) {{", TypeName, CapitalizedName); + writer.WriteLine(" return {0}_[index];", Name); + writer.WriteLine("}"); + } + + public void GenerateBuilderMembers(TextGenerator writer) { + // Note: We can return the original list here, because we make it unmodifiable when we build + writer.WriteLine("public scg::IList<{0}> {1}List {{", TypeName, CapitalizedName); + writer.WriteLine(" get {{ return result.{0}_; }}", Name); + writer.WriteLine("}"); + writer.WriteLine("public int {0}Count {{", CapitalizedName); + writer.WriteLine(" get {{ return result.{0}Count; }}", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public {0} Get{1}(int index) {{", TypeName, CapitalizedName); + writer.WriteLine(" return result.Get{0}(index);", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public Builder Set{0}(int index, {1} value) {{", CapitalizedName, TypeName); + writer.WriteLine(" result.{0}_[index] = value;", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder Add{0}({1} value) {{", CapitalizedName, TypeName); + writer.WriteLine(" result.{0}_.Add(value);", Name, TypeName); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder AddRange{0}(scg::IEnumerable<{1}> values) {{", CapitalizedName, TypeName); + writer.WriteLine(" base.AddRange(values, result.{0}_);", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder Clear{0}() {{", CapitalizedName); + writer.WriteLine(" result.{0}_.Clear();", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + } + + public void GenerateMergingCode(TextGenerator writer) { + writer.WriteLine("if (other.{0}_.Count != 0) {{", Name); + writer.WriteLine(" base.AddRange(other.{0}_, result.{0}_);", Name); + writer.WriteLine("}"); + } + + public void GenerateBuildingCode(TextGenerator writer) { + writer.WriteLine("result.{0}_.MakeReadOnly();", Name); + } + + public void GenerateParsingCode(TextGenerator writer) { + // TODO(jonskeet): Make a more efficient way of doing this + writer.WriteLine("int rawValue = input.ReadEnum();"); + writer.WriteLine("if (!global::System.Enum.IsDefined(typeof({0}), rawValue)) {{", TypeName); + writer.WriteLine(" unknownFields.MergeVarintField({0}, (ulong) rawValue);", Number); + writer.WriteLine("} else {"); + writer.WriteLine(" Add{0}(({1}) rawValue);", CapitalizedName, TypeName); + writer.WriteLine("}"); + } + + public void GenerateSerializationCode(TextGenerator writer) { + writer.WriteLine("foreach ({0} element in {1}List) {{", TypeName, CapitalizedName); + writer.WriteLine(" output.WriteEnum({0}, (int) element);", Number); + writer.WriteLine("}"); + } + + public void GenerateSerializedSizeCode(TextGenerator writer) { + writer.WriteLine("foreach ({0} element in {1}List) {{", TypeName, CapitalizedName); + writer.WriteLine(" size += pb::CodedOutputStream.ComputeEnumSize({0}, (int) element);", Number); + writer.WriteLine("}"); + } + } +} diff --git a/src/ProtoGen/RepeatedMessageFieldGenerator.cs b/src/ProtoGen/RepeatedMessageFieldGenerator.cs new file mode 100644 index 00000000..08d574a9 --- /dev/null +++ b/src/ProtoGen/RepeatedMessageFieldGenerator.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Google.ProtocolBuffers.Descriptors; + +namespace Google.ProtocolBuffers.ProtoGen { + internal class RepeatedMessageFieldGenerator : FieldGeneratorBase, IFieldSourceGenerator { + + internal RepeatedMessageFieldGenerator(FieldDescriptor descriptor) + : base(descriptor) { + } + + public void GenerateMembers(TextGenerator writer) { + writer.WriteLine("private pbc::PopsicleList<{0}> {1}_ = new pbc::PopsicleList<{0}>();", TypeName, Name); + writer.WriteLine("public scg::IList<{0}> {1}List {{", TypeName, CapitalizedName); + writer.WriteLine(" get {{ return {0}_; }}", Name); + writer.WriteLine("}"); + + // TODO(jonskeet): Redundant API calls? Possibly - include for portability though. Maybe create an option. + writer.WriteLine("public int {0}Count {{", CapitalizedName); + writer.WriteLine(" get {{ return {0}_.Count; }}", Name); + writer.WriteLine("}"); + + writer.WriteLine("public {0} Get{1}(int index) {{", TypeName, CapitalizedName); + writer.WriteLine(" return {0}_[index];", Name); + writer.WriteLine("}"); + } + + public void GenerateBuilderMembers(TextGenerator writer) { + // Note: We can return the original list here, because we make it unmodifiable when we build + writer.WriteLine("public scg::IList<{0}> {1}List {{", TypeName, CapitalizedName); + writer.WriteLine(" get {{ return result.{0}_; }}", Name); + writer.WriteLine("}"); + writer.WriteLine("public int {0}Count {{", CapitalizedName); + writer.WriteLine(" get {{ return result.{0}Count; }}", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public {0} Get{1}(int index) {{", TypeName, CapitalizedName); + writer.WriteLine(" return result.Get{0}(index);", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public Builder Set{0}(int index, {1} value) {{", CapitalizedName, TypeName); + writer.WriteLine(" result.{0}_[index] = value;", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + // Extra overload for builder (just on messages) + writer.WriteLine("public Builder Set{0}(int index, {1}.Builder builderForValue) {{", CapitalizedName, TypeName); + writer.WriteLine(" result.{0}_[index] = builderForValue.Build();", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder Add{0}({1} value) {{", CapitalizedName, TypeName); + writer.WriteLine(" result.{0}_.Add(value);", Name, TypeName); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + // Extra overload for builder (just on messages) + writer.WriteLine("public Builder Add{0}({1}.Builder builderForValue) {{", CapitalizedName, TypeName); + writer.WriteLine(" result.{0}_.Add(builderForValue.Build());", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder AddRange{0}(scg::IEnumerable<{1}> values) {{", CapitalizedName, TypeName); + writer.WriteLine(" base.AddRange(values, result.{0}_);", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder Clear{0}() {{", CapitalizedName); + writer.WriteLine(" result.{0}_.Clear();", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + } + + public void GenerateMergingCode(TextGenerator writer) { + writer.WriteLine("if (other.{0}_.Count != 0) {{", Name); + writer.WriteLine(" base.AddRange(other.{0}_, result.{0}_);", Name); + writer.WriteLine("}"); + } + + public void GenerateBuildingCode(TextGenerator writer) { + writer.WriteLine("result.{0}_.MakeReadOnly();", Name); + } + + public void GenerateParsingCode(TextGenerator writer) { + writer.WriteLine("{0}.Builder subBuilder = {0}.CreateBuilder();", TypeName); + if (Descriptor.FieldType == FieldType.Group) { + writer.WriteLine("input.ReadGroup({0}, subBuilder, extensionRegistry);", Number); + } else { + writer.WriteLine("input.ReadMessage(subBuilder, extensionRegistry);"); + } + writer.WriteLine("Add{0}(subBuilder.BuildPartial());", CapitalizedName); + } + + public void GenerateSerializationCode(TextGenerator writer) { + writer.WriteLine("foreach ({0} element in {1}List) {{", TypeName, CapitalizedName); + writer.WriteLine(" output.Write{0}({1}, element);", MessageOrGroup, Number); + writer.WriteLine("}"); + } + + public void GenerateSerializedSizeCode(TextGenerator writer) { + writer.WriteLine("foreach ({0} element in {1}List) {{", TypeName, CapitalizedName); + writer.WriteLine(" size += pb::CodedOutputStream.Compute{0}Size({1}, element);", MessageOrGroup, Number); + writer.WriteLine("}"); + } + } +} diff --git a/src/ProtoGen/RepeatedPrimitiveFieldGenerator.cs b/src/ProtoGen/RepeatedPrimitiveFieldGenerator.cs new file mode 100644 index 00000000..cdb7cd6f --- /dev/null +++ b/src/ProtoGen/RepeatedPrimitiveFieldGenerator.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Google.ProtocolBuffers.Descriptors; + +namespace Google.ProtocolBuffers.ProtoGen { + internal class RepeatedPrimitiveFieldGenerator : FieldGeneratorBase, IFieldSourceGenerator { + + internal RepeatedPrimitiveFieldGenerator(FieldDescriptor descriptor) + : base(descriptor) { + } + + public void GenerateMembers(TextGenerator writer) { + writer.WriteLine("private pbc::PopsicleList<{0}> {1}_ = new pbc::PopsicleList<{0}>();", TypeName, Name); + writer.WriteLine("public scg::IList<{0}> {1}List {{", TypeName, CapitalizedName); + writer.WriteLine(" get {{ return pbc::Lists.AsReadOnly({0}_); }}", Name); + writer.WriteLine("}"); + + // TODO(jonskeet): Redundant API calls? Possibly - include for portability though. Maybe create an option. + writer.WriteLine("public int {0}Count {{", CapitalizedName); + writer.WriteLine(" get {{ return {0}_.Count; }}", Name); + writer.WriteLine("}"); + + writer.WriteLine("public {0} Get{1}(int index) {{", TypeName, CapitalizedName); + writer.WriteLine(" return {0}_[index];", Name); + writer.WriteLine("}"); + } + + public void GenerateBuilderMembers(TextGenerator writer) { + // Note: We can return the original list here, because we make it unmodifiable when we build + writer.WriteLine("public scg::IList<{0}> {1}List {{", TypeName, CapitalizedName); + writer.WriteLine(" get {{ return result.{0}_; }}", Name); + writer.WriteLine("}"); + writer.WriteLine("public int {0}Count {{", CapitalizedName); + writer.WriteLine(" get {{ return result.{0}Count; }}", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public {0} Get{1}(int index) {{", TypeName, CapitalizedName); + writer.WriteLine(" return result.Get{0}(index);", CapitalizedName); + writer.WriteLine("}"); + writer.WriteLine("public Builder Set{0}(int index, {1} value) {{", CapitalizedName, TypeName); + writer.WriteLine(" result.{0}_[index] = value;", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder Add{0}({1} value) {{", CapitalizedName, TypeName); + writer.WriteLine(" result.{0}_.Add(value);", Name, TypeName); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder AddRange{0}(scg::IEnumerable<{1}> values) {{", CapitalizedName, TypeName); + writer.WriteLine(" base.AddRange(values, result.{0}_);", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + writer.WriteLine("public Builder Clear{0}() {{", CapitalizedName); + writer.WriteLine(" result.{0}_.Clear();", Name); + writer.WriteLine(" return this;"); + writer.WriteLine("}"); + } + + public void GenerateMergingCode(TextGenerator writer) { + writer.WriteLine("if (other.{0}_.Count != 0) {{", Name); + writer.WriteLine(" base.AddRange(other.{0}_, result.{0}_);", Name); + writer.WriteLine("}"); + } + + public void GenerateBuildingCode(TextGenerator writer) { + writer.WriteLine("result.{0}_.MakeReadOnly();", Name); + } + + public void GenerateParsingCode(TextGenerator writer) { + writer.WriteLine("Add{0}(input.Read{1}());", CapitalizedName, CapitalizedTypeName); + } + + public void GenerateSerializationCode(TextGenerator writer) { + writer.WriteLine("foreach ({0} element in {1}List) {{", TypeName, CapitalizedName); + writer.WriteLine(" output.Write{0}({1}, element);", CapitalizedTypeName, Number); + writer.WriteLine("}"); + } + + public void GenerateSerializedSizeCode(TextGenerator writer) { + writer.WriteLine("foreach ({0} element in {1}List) {{", TypeName, CapitalizedName); + writer.WriteLine(" size += pb::CodedOutputStream.Compute{0}Size({1}, element);", CapitalizedTypeName, Number); + writer.WriteLine("}"); + } + } +} diff --git a/src/ProtoGen/ServiceGenerator.cs b/src/ProtoGen/ServiceGenerator.cs new file mode 100644 index 00000000..089172db --- /dev/null +++ b/src/ProtoGen/ServiceGenerator.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Google.ProtocolBuffers.Descriptors; + +namespace Google.ProtocolBuffers.ProtoGen { + internal class ServiceGenerator : SourceGeneratorBase<ServiceDescriptor>, ISourceGenerator { + + private enum RequestOrResponse { + Request, + Response + } + + internal ServiceGenerator(ServiceDescriptor descriptor) + : base(descriptor) { + } + + public void Generate(TextGenerator writer) { + writer.WriteLine("{0} abstract class {1} : pb::IService {{", ClassAccessLevel, Descriptor.Name); + writer.Indent(); + + foreach (MethodDescriptor method in Descriptor.Methods) { + writer.WriteLine("{0} abstract void {1}(", ClassAccessLevel, Helpers.UnderscoresToPascalCase(method.Name)); + writer.WriteLine(" pb::IRpcController controller,"); + writer.WriteLine(" {0} request,", DescriptorUtil.GetClassName(method.InputType)); + writer.WriteLine(" global::System.Action<{0}> done);", DescriptorUtil.GetClassName(method.OutputType)); + } + + // Generate Descriptor and DescriptorForType. + writer.WriteLine(); + writer.WriteLine("{0} static pbd::ServiceDescriptor Descriptor {{", ClassAccessLevel); + writer.WriteLine(" get {{ return {0}.Descriptor.Services[{1}]; }}", + DescriptorUtil.GetUmbrellaClassName(Descriptor.File), Descriptor.Index); + writer.WriteLine("}"); + writer.WriteLine("{0} pbd::ServiceDescriptor DescriptorForType {{", ClassAccessLevel); + writer.WriteLine(" get { return Descriptor; }"); + writer.WriteLine("}"); + + GenerateCallMethod(writer); + GenerateGetPrototype(RequestOrResponse.Request, writer); + GenerateGetPrototype(RequestOrResponse.Response, writer); + GenerateStub(writer); + + writer.Outdent(); + writer.WriteLine("}"); + } + + private void GenerateCallMethod(TextGenerator writer) { + writer.WriteLine(); + writer.WriteLine("public void CallMethod(", ClassAccessLevel); + writer.WriteLine(" pbd::MethodDescriptor method,"); + writer.WriteLine(" pb::IRpcController controller,"); + writer.WriteLine(" pb::IMessage request,"); + writer.WriteLine(" global::System.Action<pb::IMessage> done) {"); + writer.Indent(); + writer.WriteLine("if (method.Service != Descriptor) {"); + writer.WriteLine(" throw new global::System.ArgumentException("); + writer.WriteLine(" \"Service.CallMethod() given method descriptor for wrong service type.\");"); + writer.WriteLine("}"); + writer.WriteLine("switch(method.Index) {"); + writer.Indent(); + foreach (MethodDescriptor method in Descriptor.Methods) { + writer.WriteLine("case {0}:", method.Index); + writer.WriteLine(" this.{0}(controller, ({1}) request,", + Helpers.UnderscoresToPascalCase(method.Name), DescriptorUtil.GetClassName(method.InputType)); + writer.WriteLine(" pb::RpcUtil.SpecializeCallback<{0}>(", DescriptorUtil.GetClassName(method.OutputType)); + writer.WriteLine(" done));"); + writer.WriteLine(" return;"); + } + writer.WriteLine("default:"); + writer.WriteLine(" throw new global::System.InvalidOperationException(\"Can't get here.\");"); + writer.Outdent(); + writer.WriteLine("}"); + writer.Outdent(); + writer.WriteLine("}"); + writer.WriteLine(); + } + + private void GenerateGetPrototype(RequestOrResponse which, TextGenerator writer) { + writer.WriteLine("public pb::IMessage Get{0}Prototype(pbd::MethodDescriptor method) {{", which); + writer.Indent(); + writer.WriteLine("if (method.Service != Descriptor) {"); + writer.WriteLine(" throw new global::System.ArgumentException("); + writer.WriteLine(" \"Service.Get{0}Prototype() given method descriptor for wrong service type.\");", which); + writer.WriteLine("}"); + writer.WriteLine("switch(method.Index) {"); + writer.Indent(); + + foreach (MethodDescriptor method in Descriptor.Methods) { + writer.WriteLine("case {0}:", method.Index); + writer.WriteLine(" return {0}.DefaultInstance;", + DescriptorUtil.GetClassName(which == RequestOrResponse.Request ? method.InputType : method.OutputType)); + } + writer.WriteLine("default:"); + writer.WriteLine(" throw new global::System.InvalidOperationException(\"Can't get here.\");"); + writer.Outdent(); + writer.WriteLine("}"); + writer.Outdent(); + writer.WriteLine("}"); + writer.WriteLine(); + } + + private void GenerateStub(TextGenerator writer) { + writer.WriteLine("public static Stub CreateStub(pb::IRpcChannel channel) {"); + writer.WriteLine(" return new Stub(channel);"); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("{0} class Stub : {1} {{", ClassAccessLevel, DescriptorUtil.GetClassName(Descriptor)); + writer.Indent(); + writer.WriteLine("internal Stub(pb::IRpcChannel channel) {"); + writer.WriteLine(" this.channel = channel;"); + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("private readonly pb::IRpcChannel channel;"); + writer.WriteLine(); + writer.WriteLine("public pb::IRpcChannel Channel {"); + writer.WriteLine(" get { return channel; }"); + writer.WriteLine("}"); + + foreach (MethodDescriptor method in Descriptor.Methods) { + writer.WriteLine(); + writer.WriteLine("public override void {0}(", Helpers.UnderscoresToPascalCase(method.Name)); + writer.WriteLine(" pb::IRpcController controller,"); + writer.WriteLine(" {0} request,", DescriptorUtil.GetClassName(method.InputType)); + writer.WriteLine(" global::System.Action<{0}> done) {{", DescriptorUtil.GetClassName(method.OutputType)); + writer.Indent(); + writer.WriteLine("channel.CallMethod(Descriptor.Methods[{0}],", method.Index); + writer.WriteLine(" controller, request, {0}.DefaultInstance,", DescriptorUtil.GetClassName(method.OutputType)); + writer.WriteLine(" pb::RpcUtil.GeneralizeCallback<{0}, {0}.Builder>(done, {0}.DefaultInstance));", + DescriptorUtil.GetClassName(method.OutputType)); + writer.Outdent(); + writer.WriteLine("}"); + } + writer.Outdent(); + writer.WriteLine("}"); + } + } +} diff --git a/src/ProtoGen/SourceFileGenerator.cs b/src/ProtoGen/SourceFileGenerator.cs new file mode 100644 index 00000000..1984d297 --- /dev/null +++ b/src/ProtoGen/SourceFileGenerator.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Google.ProtocolBuffers.Descriptors; + +namespace Google.ProtocolBuffers.ProtoGen { + /// <summary> + /// Generator to hold a TextGenerator, generate namespace aliases etc. + /// Each source file created uses one of these, and it can be used to create + /// multiple classes within the same file. + /// </summary> + internal class SourceFileGenerator { + + private readonly TextGenerator output; + + private SourceFileGenerator(TextWriter writer) { + output = new TextGenerator(writer); + } + + /// <summary> + /// Creates a ClassFileGenerator for the given writer, which will be closed + /// when the instance is disposed. The specified namespace is created, if it's non-null. + /// </summary> + internal static SourceFileGenerator ForWriter(TextWriter writer) { + return new SourceFileGenerator(writer); + } + } +} diff --git a/src/ProtoGen/SourceGeneratorBase.cs b/src/ProtoGen/SourceGeneratorBase.cs new file mode 100644 index 00000000..563c64a1 --- /dev/null +++ b/src/ProtoGen/SourceGeneratorBase.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using Google.ProtocolBuffers.DescriptorProtos; +using Google.ProtocolBuffers.Descriptors; + +namespace Google.ProtocolBuffers.ProtoGen { + internal abstract class SourceGeneratorBase<T> where T : IDescriptor { + + private readonly T descriptor; + + protected SourceGeneratorBase(T descriptor) { + this.descriptor = descriptor; + } + + protected T Descriptor { + get { return descriptor; } + } + + protected string ClassAccessLevel { + get { + // Default to public + return !descriptor.File.Options.HasExtension(CSharpOptions.CSharpPublicClasses) + || descriptor.File.Options.GetExtension(CSharpOptions.CSharpPublicClasses) ? "public" : "internal"; + } + } + + public bool MultipleFiles { + get { return descriptor.File.Options.GetExtension(CSharpOptions.CSharpMultipleFiles); } + } + + protected static void WriteChildren<TChild>(TextGenerator writer, string region, IEnumerable<TChild> children) + where TChild : IDescriptor { + // Copy the set of children; makes access easier + List<TChild> copy = new List<TChild>(children); + if (copy.Count == 0) { + return; + } + + if (region != null) { + writer.WriteLine("#region {0}", region); + } + foreach (TChild child in children) { + SourceGenerators.CreateGenerator(child).Generate(writer); + } + if (region != null) { + writer.WriteLine("#endregion"); + writer.WriteLine(); + } + } + } +} diff --git a/src/ProtoGen/SourceGenerators.cs b/src/ProtoGen/SourceGenerators.cs new file mode 100644 index 00000000..c775e419 --- /dev/null +++ b/src/ProtoGen/SourceGenerators.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using Google.ProtocolBuffers.Descriptors; + +namespace Google.ProtocolBuffers.ProtoGen { + internal static class SourceGenerators { + + private static readonly Dictionary<Type, Func<IDescriptor, ISourceGenerator>> GeneratorFactories = new Dictionary<Type, Func<IDescriptor, ISourceGenerator>> { + { typeof(FileDescriptor), descriptor => new UmbrellaClassGenerator((FileDescriptor) descriptor) }, + { typeof(EnumDescriptor), descriptor => new EnumGenerator((EnumDescriptor) descriptor) }, + { typeof(ServiceDescriptor), descriptor => new ServiceGenerator((ServiceDescriptor) descriptor) }, + { typeof(MessageDescriptor), descriptor => new MessageGenerator((MessageDescriptor) descriptor) }, + // For other fields, we have IFieldSourceGenerators. + { typeof(FieldDescriptor), descriptor => new ExtensionGenerator((FieldDescriptor) descriptor) } + }; + + public static IFieldSourceGenerator CreateFieldGenerator(FieldDescriptor field) { + switch (field.MappedType) { + case MappedType.Message : + return field.IsRepeated + ? (IFieldSourceGenerator) new RepeatedMessageFieldGenerator(field) + : new MessageFieldGenerator(field); + case MappedType.Enum: + return field.IsRepeated + ? (IFieldSourceGenerator)new RepeatedEnumFieldGenerator(field) + : new EnumFieldGenerator(field); + default: + return field.IsRepeated + ? (IFieldSourceGenerator)new RepeatedPrimitiveFieldGenerator(field) + : new PrimitiveFieldGenerator(field); + } + } + + public static ISourceGenerator CreateGenerator<T>(T descriptor) where T : IDescriptor { + Func<IDescriptor, ISourceGenerator> factory; + if (!GeneratorFactories.TryGetValue(typeof(T), out factory)) { + throw new ArgumentException("No generator registered for " + typeof(T).Name); + } + return factory(descriptor); + } + } +} diff --git a/src/ProtoGen/UmbrellaClassGenerator.cs b/src/ProtoGen/UmbrellaClassGenerator.cs new file mode 100644 index 00000000..7db5c46c --- /dev/null +++ b/src/ProtoGen/UmbrellaClassGenerator.cs @@ -0,0 +1,96 @@ +using System; +using Google.ProtocolBuffers.DescriptorProtos; +using Google.ProtocolBuffers.Descriptors; + +namespace Google.ProtocolBuffers.ProtoGen { + /// <summary> + /// Generator for the class describing the .proto file in general, + /// containing things like the message descriptor. + /// </summary> + internal sealed class UmbrellaClassGenerator : SourceGeneratorBase<FileDescriptor>, ISourceGenerator { + + internal UmbrellaClassGenerator(FileDescriptor descriptor) + : base(descriptor) { + } + + public void Generate(TextGenerator writer) { + WriteIntroduction(writer); + WriteDescriptor(writer); + WriteChildren(writer, "Extensions", Descriptor.Extensions); + writer.WriteLine("#region Static variables"); + foreach (MessageDescriptor message in Descriptor.MessageTypes) { + new MessageGenerator(message).GenerateStaticVariables(writer); + } + writer.WriteLine("#endregion"); + // The class declaration either gets closed before or after the children are written. + if (!DescriptorUtil.NestClasses(Descriptor)) { + writer.Outdent(); + writer.WriteLine("}"); + } + WriteChildren(writer, "Enums", Descriptor.EnumTypes); + WriteChildren(writer, "Messages", Descriptor.MessageTypes); + WriteChildren(writer, "Services", Descriptor.Services); + if (DescriptorUtil.NestClasses(Descriptor)) { + writer.Outdent(); + writer.WriteLine("}"); + } + if (DescriptorUtil.GetNamespace(Descriptor) != "") { + writer.Outdent(); + writer.WriteLine("}"); + } + } + + private void WriteIntroduction(TextGenerator writer) { + writer.WriteLine("// Generated by the protocol buffer compiler. DO NOT EDIT!"); + writer.WriteLine(); + Helpers.WriteNamespaces(writer); + + if (DescriptorUtil.GetNamespace(Descriptor) != "") { + writer.WriteLine("namespace {0} {{", DescriptorUtil.GetNamespace(Descriptor)); + writer.Indent(); + writer.WriteLine(); + } + + writer.WriteLine("{0} static partial class {1} {{", ClassAccessLevel, DescriptorUtil.GetUmbrellaClassName(Descriptor)); + writer.WriteLine(); + writer.Indent(); + } + + private void WriteDescriptor(TextGenerator writer) { + writer.WriteLine("#region Descriptor"); + + writer.WriteLine("public static pbd::FileDescriptor Descriptor {"); + writer.WriteLine(" get { return descriptor; }"); + writer.WriteLine("}"); + writer.WriteLine("private static readonly pbd::FileDescriptor descriptor = pbd::FileDescriptor.InternalBuildGeneratedFileFrom("); + writer.WriteLine(" global::System.Convert.FromBase64String("); + writer.Indent(); + writer.Indent(); + + // TODO(jonskeet): Consider a C#-escaping format here instead of just Base64. + byte[] bytes = Descriptor.Proto.ToByteArray(); + string base64 = Convert.ToBase64String(bytes); + + while (base64.Length > 60) { + writer.WriteLine("\"{0}\" + ", base64.Substring(0, 60)); + base64 = base64.Substring(60); + } + writer.WriteLine("\"{0}\"),", base64); + + writer.WriteLine("new pbd::FileDescriptor[] {"); + foreach (FileDescriptor dependency in Descriptor.Dependencies) { + // TODO(jonskeet): The normal code won't work for the bootstrapping descriptor, because we don't get unknown fields :( + if (dependency.Package == "google.protobuf" && dependency.Name.EndsWith("descriptor.proto")) { + writer.WriteLine(" global::" + typeof(DescriptorProtoFile).FullName + ".Descriptor, "); + continue; + } + writer.WriteLine(" {0}.Descriptor, ", DescriptorUtil.GetFullUmbrellaClassName(dependency)); + } + writer.WriteLine("});"); + writer.Outdent(); + writer.Outdent(); + writer.WriteLine("#endregion"); + writer.WriteLine(); + } + } +} diff --git a/src/ProtoGen/app.config b/src/ProtoGen/app.config new file mode 100644 index 00000000..df20690a --- /dev/null +++ b/src/ProtoGen/app.config @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<configuration> +<startup><supportedRuntime version="v2.0.50727"/></startup></configuration> |