]> asedeno.scripts.mit.edu Git - cl-protobufs.git/blobdiff - define-proto.lisp
Generate integer encoders and decoders with thought towards the size of a FIXNUM
[cl-protobufs.git] / define-proto.lisp
index c797d04f13ba88af280adf9e4d3842933e5a4e91..5944fa8448289cbbdc231ecebb4d75d197547c02 100644 (file)
@@ -1,8 +1,8 @@
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;                                                                  ;;;
-;;; Confidential and proprietary information of ITA Software, Inc.   ;;;
+;;; Free Software published under an MIT-like license. See LICENSE   ;;;
 ;;;                                                                  ;;;
-;;; Copyright (c) 2012 ITA Software, Inc.  All rights reserved.      ;;;
+;;; Copyright (c) 2012 Google, Inc.  All rights reserved.            ;;;
 ;;;                                                                  ;;;
 ;;; Original author: Scott McKay                                     ;;;
 ;;;                                                                  ;;;
@@ -14,8 +14,9 @@
 ;;; Protocol buffer defining macros
 
 ;; Define a schema named 'type', corresponding to a .proto file of that name
-(defmacro define-proto (type (&key name syntax package lisp-package import optimize options documentation)
-                        &body messages &environment env)
+(defmacro define-schema (type (&key name syntax package lisp-package import optimize
+                                    options documentation)
+                         &body messages &environment env)
   "Define a schema named 'type', corresponding to a .proto file of that name.
    'name' can be used to override the defaultly generated Protobufs name.
    'syntax' and 'package' are as they would be in a .proto file.
   (let* ((name     (or name (class-name->proto type)))
          (package  (and package (if (stringp package) package (string-downcase (string package)))))
          (lisp-pkg (and lisp-package (if (stringp lisp-package) lisp-package (string lisp-package))))
-         (options  (loop for (key val) on options by #'cddr
-                         collect (make-instance 'protobuf-option
-                                   :name  key
-                                   :value val)))
+         (options  (remove-options
+                     (loop for (key val) on options by #'cddr
+                           collect (make-instance 'protobuf-option
+                                     :name  (if (symbolp key) (slot-name->proto key) key)
+                                     :value val))
+                     "optimize_for" "lisp_package"))
          (imports  (if (listp import) import (list import)))
-         (protobuf (make-instance 'protobuf
+         (schema   (make-instance 'protobuf-schema
                      :class    type
                      :name     name
                      :syntax   (or syntax "proto2")
                      :package  package
-                     :lisp-package (or lisp-pkg package)
+                     :lisp-package (or lisp-pkg (substitute #\- #\_ package))
                      :imports  imports
                      :options  (if optimize
                                  (append options (list (make-instance 'protobuf-option
                                                          :type  'symbol)))
                                  options)
                      :documentation documentation))
-         (*protobuf* protobuf)
-         (*protobuf-package* (or (find-package lisp-pkg)
-                                 (find-package (string-upcase lisp-pkg))
-                                 *package*)))
-    (apply #'process-imports imports)
+         (*protobuf* schema)
+         (*protobuf-package* (or (find-proto-package lisp-pkg) *package*)))
+    (process-imports schema imports)
     (with-collectors ((forms collect-form))
       (dolist (msg messages)
         (assert (and (listp msg)
-                     (member (car msg) '(define-enum define-message define-extend define-service))) ()
+                     (member (car msg) '(define-enum define-message define-extend define-service
+                                         define-type-alias))) ()
                 "The body of ~S must be one of ~{~S~^ or ~}"
-                'define-proto '(define-enum define-message define-extend define-service))
+                'define-schema
+                '(define-enum define-message define-extend define-service define-type-alias))
         ;; The macro-expander will return a form that consists
         ;; of 'progn' followed by a symbol naming what we've expanded
         ;; (define-enum, define-message, define-extend, define-service),
         ;; followed by the Lisp model object created by the defining form,
         ;; followed by other defining forms (e.g., deftype, defclass)
-        (destructuring-bind (&optional progn type model definers)
+        (destructuring-bind (&optional progn model-type model definers)
             (macroexpand-1 msg env)
           (assert (eq progn 'progn) ()
                   "The macroexpansion for ~S failed" msg)
           (map () #'collect-form definers)
-          (ecase type
+          (ecase model-type
             ((define-enum)
-             (setf (proto-enums protobuf) (nconc (proto-enums protobuf) (list model))))
+             (setf (proto-enums schema) (nconc (proto-enums schema) (list model))))
+            ((define-type-alias)
+             (setf (proto-type-aliases schema) (nconc (proto-type-aliases schema) (list model))))
             ((define-message define-extend)
-             (setf (proto-parent model) protobuf)
-             (setf (proto-messages protobuf) (nconc (proto-messages protobuf) (list model)))
+             (setf (proto-parent model) schema)
+             (setf (proto-messages schema) (nconc (proto-messages schema) (list model)))
              (when (eq (proto-message-type model) :extends)
-               (setf (proto-extenders protobuf) (nconc (proto-extenders protobuf) (list model)))))
+               (setf (proto-extenders schema) (nconc (proto-extenders schema) (list model)))))
             ((define-service)
-             (setf (proto-services protobuf) (nconc (proto-services protobuf) (list model)))))))
+             (setf (proto-services schema) (nconc (proto-services schema) (list model)))))))
       (let ((var (intern (format nil "*~A*" type) *protobuf-package*)))
         `(progn
            ,@forms
            (defvar ,var nil)
-           (let* ((old-proto ,var)
-                  (new-proto ,protobuf))
-             (when old-proto
+           (let* ((old-schema ,var)
+                  (new-schema ,schema))
+             (when old-schema
                (multiple-value-bind (upgradable warnings)
-                   (protobuf-upgradable old-proto new-proto)
+                   (schema-upgradable old-schema new-schema)
                  (unless upgradable
                    (protobufs-warn "The old schema for ~S (~A) can't be safely upgraded; proceeding anyway"
                                    ',type ',name)
                    (map () #'protobufs-warn warnings))))
-             (setq ,var new-proto)
-             ,@(when (eq optimize :speed)
-                 (with-collectors ((messages collect-message))
-                   (labels ((collect-messages (message)
-                              (collect-message message)
-                              (map () #'collect-messages (proto-messages message))))
-                   (map () #'collect-messages (proto-messages protobuf)))
-                   (append (mapcar #'generate-object-size  messages)
-                           (mapcar #'generate-serializer   messages)
-                           (mapcar #'generate-deserializer messages))))
-             new-proto))))))
+             (setq ,var new-schema)
+             (record-protobuf ,var))
+           ,@(with-collectors ((messages collect-message))
+               (labels ((collect-messages (message)
+                          (collect-message message)
+                          (map () #'collect-messages (proto-messages message))))
+                 (map () #'collect-messages (proto-messages schema)))
+               (append 
+                (mapcar #'(lambda (m) `(record-protobuf ,m)) messages)
+                (when (eq optimize :speed)
+                  (append (mapcar #'generate-object-size  messages)
+                          (mapcar #'generate-serializer   messages)
+                          (mapcar #'generate-deserializer messages)))))
+           ,var)))))
+
+(defmacro with-proto-source-location ((type name definition-type
+                                       &optional pathname start-pos end-pos)
+                                      &body body)
+  "Establish a context which causes the generated Lisp code to have
+   source location information that points to the .proto file.
+   'type' is the name of the Lisp definition (a symbol).
+   'name' is the name of the Protobufs definition (a string).
+   'definition-type' is the kind of definition, e.g., 'protobuf-enum'.
+   'pathname', 'start-pos' and 'end-pos' give the location of the definition
+   in the .proto file."
+  `(progn
+     (record-proto-source-location ',type ,name ',definition-type
+                                   ,pathname ,start-pos ,end-pos)
+     ,@body))
+
+#+ccl
+(defun record-proto-source-location (type name definition-type
+                                     &optional pathname start-pos end-pos)
+  (declare (ignore name))
+  (when (and ccl::*record-source-file*
+             (typep pathname '(or string pathname)))
+    (let ((ccl::*loading-toplevel-location* (ccl::make-source-note :filename  pathname
+                                                                   :start-pos start-pos
+                                                                   :end-pos   end-pos)))
+      (ccl:record-source-file type definition-type))))
+
+#-(or ccl)
+(defun record-proto-source-location (type name definition-type
+                                     &optional pathname start-pos end-pos)
+  (declare (ignorable name type definition-type pathname start-pos end-pos)))
 
 ;; Define an enum type named 'type' and a Lisp 'deftype'
-(defmacro define-enum (type (&key name conc-name alias-for options documentation)
+(defmacro define-enum (type (&key name conc-name alias-for options
+                                  documentation source-location)
                        &body values)
   "Define a Protobufs enum type and a Lisp 'deftype' named 'type'.
    'name' can be used to override the defaultly generated Protobufs enum name.
   (let* ((name    (or name (class-name->proto type)))
          (options (loop for (key val) on options by #'cddr
                         collect (make-instance 'protobuf-option
-                                  :name  key
+                                  :name  (if (symbolp key) (slot-name->proto key) key)
                                   :value val)))
+         (conc-name (conc-name-for-type type conc-name))
          (index -1)
          (enum  (make-instance 'protobuf-enum
                   :class  type
                   :name   name
+                  :qualified-name (make-qualified-name *protobuf* name)
+                  :parent *protobuf*
                   :alias-for alias-for
                   :options options
-                  :documentation documentation)))
+                  :documentation documentation
+                  :source-location source-location)))
     (with-collectors ((vals  collect-val)
                       (forms collect-form))
       (dolist (val values)
-        (let* ((idx  (if (listp val) (second val) (incf index)))
+        ;; Allow old (name index) and new (name :index index)
+        (let* ((idx  (if (listp val)
+                       (if (eq (second val) :index) (third val) (second val))
+                       (incf index)))
                (name (if (listp val) (first val)  val))
                (val-name  (kintern (if conc-name (format nil "~A~A" conc-name name) (symbol-name name))))
                (enum-name (if conc-name (format nil "~A~A" conc-name name) (symbol-name name)))
+               (vname     (enum-name->proto enum-name))
                (enum-val  (make-instance 'protobuf-enum-value
-                            :name  (enum-name->proto enum-name)
-                            :index idx
-                            :value val-name)))
+                            :name   vname
+                            :qualified-name (make-qualified-name enum vname)
+                            :index  idx
+                            :value  val-name
+                            :parent enum)))
           (collect-val val-name)
           (setf (proto-values enum) (nconc (proto-values enum) (list enum-val)))))
       (if alias-for
       `(progn
          define-enum
          ,enum
-         ,forms))))
+         ((with-proto-source-location (,type ,name protobuf-enum ,@source-location)
+            ,@forms))))))
 
 ;; Define a message named 'name' and a Lisp 'defclass'
-(defmacro define-message (type (&key name conc-name alias-for options documentation)
+(defmacro define-message (type (&key name conc-name alias-for options
+                                     documentation source-location)
                           &body fields &environment env)
   "Define a message named 'type' and a Lisp 'defclass'.
    'name' can be used to override the defaultly generated Protobufs message name.
   (let* ((name    (or name (class-name->proto type)))
          (options (loop for (key val) on options by #'cddr
                         collect (make-instance 'protobuf-option
-                                  :name  key
+                                  :name  (if (symbolp key) (slot-name->proto key) key)
                                   :value val)))
+         (conc-name (conc-name-for-type type conc-name))
          (message (make-instance 'protobuf-message
                     :class type
                     :name  name
+                    :qualified-name (make-qualified-name *protobuf* name)
+                    :parent *protobuf*
                     :alias-for alias-for
-                    :conc-name (and conc-name (string conc-name))
-                    :options  options
-                    :documentation documentation))
+                    :conc-name conc-name
+                    :options   (remove-options options "default" "packed")
+                    :documentation documentation
+                    :source-location source-location))
          (index 0)
+         ;; Only now can we bind *protobuf* to the new message
          (*protobuf* message))
     (with-collectors ((slots collect-slot)
-                      (forms collect-form))
+                      (forms collect-form)
+                      ;; The typedef needs to be first in forms otherwise ccl warns.
+                      ;; We'll collect them separately and splice them in first.
+                      (type-forms collect-type-form))
       (dolist (field fields)
         (case (car field)
-          ((define-enum define-message define-extend define-extension define-group)
-           (destructuring-bind (&optional progn type model definers extra-field extra-slot)
+          ((define-enum define-message define-extend define-extension define-group
+            define-type-alias)
+           (destructuring-bind (&optional progn model-type model definers extra-field extra-slot)
                (macroexpand-1 field env)
              (assert (eq progn 'progn) ()
                      "The macroexpansion for ~S failed" field)
              (map () #'collect-form definers)
-             (ecase type
+             (ecase model-type
                ((define-enum)
                 (setf (proto-enums message) (nconc (proto-enums message) (list model))))
+               ((define-type-alias)
+                (setf (proto-type-aliases message) (nconc (proto-type-aliases message) (list model))))
                ((define-message define-extend)
                 (setf (proto-parent model) message)
                 (setf (proto-messages message) (nconc (proto-messages message) (list model)))
           (otherwise
            (multiple-value-bind (field slot idx)
                (process-field field index :conc-name conc-name :alias-for alias-for)
-             (assert (not (find (proto-index field) (proto-fields message) :key #'proto-index)) ()
+             (assert (not (find-field message (proto-index field))) ()
                      "The field ~S overlaps with another field in ~S"
                      (proto-value field) (proto-class message))
              (setq index idx)
         ;; If we've got an alias, define a a type that is the subtype of
         ;; the Lisp class that typep and subtypep work
         (unless (or (eq type alias-for) (find-class type nil))
-          (collect-form `(deftype ,type () ',alias-for)))
+          (collect-type-form `(deftype ,type () ',alias-for)))
         ;; If no alias, define the class now
-        (collect-form `(defclass ,type () (,@slots)
+        (collect-type-form `(defclass ,type () (,@slots)
                          ,@(and documentation `((:documentation ,documentation))))))
       `(progn
          define-message
          ,message
-         ,forms))))
+         ((with-proto-source-location (,type ,name protobuf-message ,@source-location)
+            ,@type-forms
+            ,@forms))))))
+
+(defun conc-name-for-type (type conc-name)
+  (and conc-name
+       (typecase conc-name
+         ((member t) (format nil "~:@(~A~)-" type))
+         ((or string symbol) (string-upcase (string conc-name)))
+         (t nil))))
 
 (defmacro define-extension (from to)
   "Define an extension range within a message.
    The \"body\" is the start and end of the range, both inclusive."
-  `(progn
-     define-extension
-     ,(make-instance 'protobuf-extension
-        :from from
-        :to   (if (eq to 'max) #.(1- (ash 1 29)) to))
-     ()))
+  (let ((to (etypecase to
+              (integer to)
+              (symbol (if (string-equal to "MAX") #.(1- (ash 1 29)) to)))))
+    `(progn
+       define-extension
+       ,(make-instance 'protobuf-extension
+          :from from
+          :to   (if (eq to 'max) #.(1- (ash 1 29)) to))
+       ())))
 
-(defmacro define-extend (type (&key name options documentation)
+(defmacro define-extend (type (&key name conc-name options documentation)
                          &body fields &environment env)
   "Define an extension to the message named 'type'.
    'name' can be used to override the defaultly generated Protobufs message name.
    'reader' is a Lisp slot reader function to use to get the value, instead of
    using 'slot-value'; this is often used when aliasing an existing class.
    'writer' is a Lisp slot writer function to use to set the value."
-  (declare (ignore env))
   (let* ((name    (or name (class-name->proto type)))
          (options (loop for (key val) on options by #'cddr
                         collect (make-instance 'protobuf-option
-                                  :name  key
+                                  :name  (if (symbolp key) (slot-name->proto key) key)
                                   :value val)))
-         (message   (find-message *protobuf* name))
-         (conc-name (and message (proto-conc-name message)))
+         (message   (find-message *protobuf* type))
+         (conc-name (or (conc-name-for-type type conc-name)
+                        (and message (proto-conc-name message))))
          (alias-for (and message (proto-alias-for message)))
          (extends (and message
                        (make-instance 'protobuf-message
-                         :class  type
-                         :name   name
-                         :parent (proto-parent message)
-                         :conc-name conc-name
+                         :class  (proto-class message)
+                         :name   (proto-name message)
+                         :qualified-name (proto-qualified-name message)
+                         :parent *protobuf*
                          :alias-for alias-for
+                         :conc-name conc-name
                          :enums    (copy-list (proto-enums message))
                          :messages (copy-list (proto-messages message))
                          :fields   (copy-list (proto-fields message))
-                         :options  (or options (copy-list (proto-options message)))
                          :extensions (copy-list (proto-extensions message))
+                         :options  (remove-options
+                                     (or options (copy-list (proto-options message))) "default" "packed")
                          :message-type :extends         ;this message is an extension
-                         :documentation documentation)))
+                         :documentation documentation
+                         :type-aliases  (copy-list (proto-type-aliases message)))))
+         ;; Only now can we bind *protobuf* to the new extended message
+         (*protobuf* extends)
          (index 0))
     (assert message ()
             "There is no message named ~A to extend" name)
     (with-collectors ((forms collect-form))
       (dolist (field fields)
         (assert (not (member (car field)
-                             '(define-enum define-message define-extend define-extension define-group))) ()
-                "The body of ~S can only contain field definitions" 'define-extend)
-        (multiple-value-bind (field slot idx)
-            (process-field field index :conc-name conc-name :alias-for alias-for)
-          (assert (not (find (proto-index field) (proto-fields extends) :key #'proto-index)) ()
-                  "The field ~S overlaps with another field in ~S"
-                  (proto-value field) (proto-class extends))
-          (assert (index-within-extensions-p idx message) ()
-                  "The index ~D is not in range for extending ~S"
-                  idx (proto-class message))
-          (setq index idx)
-          (when slot
-            (let* ((inits  (cdr slot))
-                   (sname  (car slot))
-                   (stable (fintern "~A-VALUES" sname))
-                   (stype  (getf inits :type))
-                   (reader (or (getf inits :accessor)
-                               (getf inits :reader)
-                               (intern (if conc-name (format nil "~A~A" conc-name sname) (symbol-name sname))
-                                       (symbol-package sname))))
-                   (writer (or (getf inits :writer)
-                               (intern (format nil "~A-~A" reader 'setter)
-                                       (symbol-package sname))))
-                   (default (getf inits :initform)))
-              ;; For the extended slots, each slot gets its own table
-              ;; keyed by the object, which lets us avoid having a slot in each
-              ;; instance that holds a table keyed by the slot name
-              ;; Multiple 'define-extends' on the same class in the same image
-              ;; will result in harmless redefinitions, so squelch the warnings
-              (collect-form `(without-redefinition-warnings ()
-                               (let ((,stable (make-hash-table :test #'eq :weak t)))
-                                 (defmethod ,reader ((object ,type))
-                                   (gethash object ,stable ,default))
-                                 (defmethod ,writer ((object ,type) value)
-                                   (declare (type ,stype value))
-                                   (setf (gethash object ,stable) value))
-                                 ;; For Python compatibility
-                                 (defmethod get-extension ((object ,type) (slot (eql ',sname)))
-                                   (values (gethash object ,stable ,default)))
-                                 (defmethod set-extension ((object ,type) (slot (eql ',sname)) value)
-                                   (setf (gethash object ,stable) value))
-                                 (defmethod has-extension ((object ,type) (slot (eql ',sname)))
-                                   (multiple-value-bind (value foundp)
-                                       (gethash object ,stable ,default)
-                                     (declare (ignore value))
-                                     foundp))
-                                 (defmethod clear-extension ((object ,type) (slot (eql ',sname)))
-                                   (remhash object ,stable))
-                                 (defsetf ,reader ,writer))))
-              ;; This so that (de)serialization works
-              (setf (proto-reader field) reader
-                    (proto-writer field) writer)))
-          (setf (proto-message-type field) :extends)    ;this field is an extension
-          (setf (proto-fields extends) (nconc (proto-fields extends) (list field)))))
+                             '(define-enum define-message define-extend define-extension
+                               define-type-alias))) ()
+                "The body of ~S can only contain field and group definitions" 'define-extend)
+        (case (car field)
+          ((define-group)
+           (destructuring-bind (&optional progn model-type model definers extra-field extra-slot)
+               (macroexpand-1 field env)
+             (assert (eq progn 'progn) ()
+                     "The macroexpansion for ~S failed" field)
+             (map () #'collect-form definers)
+             (ecase model-type
+               ((define-group)
+                (setf (proto-parent model) extends)
+                (setf (proto-messages extends) (nconc (proto-messages extends) (list model)))
+                (when extra-slot
+                  ;;--- Refactor to get rid of all this duplicated code!
+                  (let* ((inits  (cdr extra-slot))
+                         (sname  (car extra-slot))
+                         (stable (fintern "~A-VALUES" sname))
+                         (stype  (getf inits :type))
+                         (reader (or (getf inits :accessor)
+                                     (getf inits :reader)
+                                     (intern (if conc-name (format nil "~A~A" conc-name sname) (symbol-name sname))
+                                             *protobuf-package*)))
+                         (writer (or (getf inits :writer)
+                                     (intern (format nil "~A-~A" 'set reader) *protobuf-package*)))
+                         (default (getf inits :initform)))
+                    (collect-form `(without-redefinition-warnings ()
+                                     (let ((,stable #+ccl  (make-hash-table :test #'eq :weak t)
+                                                    #+sbcl (make-hash-table :test #'eq :weakness :value)))
+                                       ,@(and reader `((defmethod ,reader ((object ,type))
+                                                         (gethash object ,stable ,default))))
+                                       ,@(and writer `((defmethod ,writer ((object ,type) value)
+                                                         (declare (type ,stype value))
+                                                         (setf (gethash object ,stable) value))))
+                                       ;; For Python compatibility
+                                       (defmethod get-extension ((object ,type) (slot (eql ',sname)))
+                                         (values (gethash object ,stable ,default)))
+                                       (defmethod set-extension ((object ,type) (slot (eql ',sname)) value)
+                                         (setf (gethash object ,stable) value))
+                                       (defmethod has-extension ((object ,type) (slot (eql ',sname)))
+                                         (multiple-value-bind (value foundp)
+                                             (gethash object ,stable)
+                                           (declare (ignore value))
+                                           foundp))
+                                       (defmethod clear-extension ((object ,type) (slot (eql ',sname)))
+                                         (remhash object ,stable)))
+                                     ,@(and writer
+                                            ;; 'defsetf' needs to be visible at compile time
+                                            `((eval-when (:compile-toplevel :load-toplevel :execute)
+                                                (defsetf ,reader ,writer))))))))
+                (setf (proto-message-type extra-field) :extends) ;this field is an extension
+                (setf (proto-fields extends) (nconc (proto-fields extends) (list extra-field)))
+                (setf (proto-extended-fields extends) (nconc (proto-extended-fields extends) (list extra-field)))))))
+          (otherwise
+           (multiple-value-bind (field slot idx)
+               (process-field field index :conc-name conc-name :alias-for alias-for)
+             (assert (not (find-field extends (proto-index field))) ()
+                     "The field ~S overlaps with another field in ~S"
+                     (proto-value field) (proto-class extends))
+             (assert (index-within-extensions-p idx message) ()
+                     "The index ~D is not in range for extending ~S"
+                     idx (proto-class message))
+             (setq index idx)
+             (when slot
+               (let* ((inits  (cdr slot))
+                      (sname  (car slot))
+                      (stable (fintern "~A-VALUES" sname))
+                      (stype  (getf inits :type))
+                      (reader (or (getf inits :accessor)
+                                  (getf inits :reader)
+                                  (intern (if conc-name (format nil "~A~A" conc-name sname) (symbol-name sname))
+                                          *protobuf-package*)))
+                      (writer (or (getf inits :writer)
+                                  (intern (format nil "~A-~A" 'set reader) *protobuf-package*)))
+                      (default (getf inits :initform)))
+                 ;; For the extended slots, each slot gets its own table
+                 ;; keyed by the object, which lets us avoid having a slot in each
+                 ;; instance that holds a table keyed by the slot name
+                 ;; Multiple 'define-extends' on the same class in the same image
+                 ;; will result in harmless redefinitions, so squelch the warnings
+                 ;;--- Maybe these methods need to be defined in 'define-message'?
+                 (collect-form `(without-redefinition-warnings ()
+                                  (let ((,stable #+ccl  (make-hash-table :test #'eq :weak t)
+                                                 #+sbcl (make-hash-table :test #'eq :weakness :value)))
+                                    ,@(and reader `((defmethod ,reader ((object ,type))
+                                                      (gethash object ,stable ,default))))
+                                    ,@(and writer `((defmethod ,writer ((object ,type) value)
+                                                      (declare (type ,stype value))
+                                                      (setf (gethash object ,stable) value))))
+                                    (defmethod get-extension ((object ,type) (slot (eql ',sname)))
+                                      (values (gethash object ,stable ,default)))
+                                    (defmethod set-extension ((object ,type) (slot (eql ',sname)) value)
+                                      (setf (gethash object ,stable) value))
+                                    (defmethod has-extension ((object ,type) (slot (eql ',sname)))
+                                      (multiple-value-bind (value foundp)
+                                          (gethash object ,stable)
+                                        (declare (ignore value))
+                                        foundp))
+                                    (defmethod clear-extension ((object ,type) (slot (eql ',sname)))
+                                      (remhash object ,stable)))
+                                  ,@(and writer
+                                         `((eval-when (:compile-toplevel :load-toplevel :execute)
+                                             (defsetf ,reader ,writer))))))
+                 ;; This so that (de)serialization works
+                 (setf (proto-reader field) reader
+                       (proto-writer field) writer)))
+             (setf (proto-message-type field) :extends)         ;this field is an extension
+             (setf (proto-fields extends) (nconc (proto-fields extends) (list field)))
+             (setf (proto-extended-fields extends) (nconc (proto-extended-fields extends) (list field)))))))
       `(progn
          define-extend
          ,extends
                    (i<= index (proto-extension-to ext))))
           extensions)))
 
-(defmacro define-group (type (&key index arity name conc-name alias-for options documentation)
+(defmacro define-group (type (&key index arity name conc-name alias-for reader options
+                                   documentation source-location)
                         &body fields &environment env)
   "Define a message named 'type' and a Lisp 'defclass', *and* a field named type.
    This is deprecated in Protobufs, but if you have to use it, you must give
    'writer' is a Lisp slot writer function to use to set the value."
   (check-type index integer)
   (check-type arity (member :required :optional :repeated))
-  (let* ((slot    (or (and name (proto->slot-name name)) type))
+  (let* ((slot    (or type (and name (proto->slot-name name *protobuf-package*))))
          (name    (or name (class-name->proto type)))
          (options (loop for (key val) on options by #'cddr
                         collect (make-instance 'protobuf-option
-                                  :name  key
+                                  :name  (if (symbolp key) (slot-name->proto key) key)
                                   :value val)))
+         (conc-name (conc-name-for-type type conc-name))
+         (reader  (or reader
+                      (let ((msg-conc (proto-conc-name *protobuf*)))
+                        (and msg-conc
+                             (intern (format nil "~A~A" msg-conc slot) *protobuf-package*)))))
          (mslot   (unless alias-for
                     `(,slot ,@(case arity
                                 (:required
                                 (:repeated
                                  `(:type (list-of ,type)
                                    :initform ())))
+                            ,@(and reader
+                                   `(:accessor ,reader))
                             :initarg ,(kintern (symbol-name slot)))))
          (mfield  (make-instance 'protobuf-field
                     :name  (slot-name->proto slot)
-                    :value slot
                     :type  name
                     :class type
-                    ;; One of :required, :optional or :repeated
+                    :qualified-name (make-qualified-name *protobuf* (slot-name->proto slot))
+                    :parent *protobuf*
                     :required arity
                     :index index
+                    :value slot
+                    :reader reader
                     :message-type :group))
          (message (make-instance 'protobuf-message
                     :class type
                     :name  name
+                    :qualified-name (make-qualified-name *protobuf* name)
+                    :parent *protobuf*
                     :alias-for alias-for
-                    :conc-name (and conc-name (string conc-name))
-                    :options  options
+                    :conc-name conc-name
+                    :options   (remove-options options "default" "packed")
                     :message-type :group                ;this message is a group
-                    :documentation documentation))
+                    :documentation documentation
+                    :source-location source-location))
          (index 0)
+         ;; Only now can we bind *protobuf* to the (group) message
          (*protobuf* message))
     (with-collectors ((slots collect-slot)
-                      (forms collect-form))
+                      (forms collect-form)
+                      ;; The typedef needs to be first in forms otherwise ccl warns.
+                      ;; We'll collect them separately and splice them in first.
+                      (type-forms collect-type-form))
       (dolist (field fields)
         (case (car field)
-          ((define-enum define-message define-extend define-extension define-group)
-           (destructuring-bind (&optional progn type model definers extra-field extra-slot)
+          ((define-enum define-message define-extend define-extension define-group
+            define-type-alias)
+           (destructuring-bind (&optional progn model-type model definers extra-field extra-slot)
                (macroexpand-1 field env)
              (assert (eq progn 'progn) ()
                      "The macroexpansion for ~S failed" field)
              (map () #'collect-form definers)
-             (ecase type
+             (ecase model-type
                ((define-enum)
                 (setf (proto-enums message) (nconc (proto-enums message) (list model))))
+               ((define-type-alias)
+                (setf (proto-type-aliases message) (nconc (proto-type-aliases message) (list model))))
                ((define-message define-extend)
                 (setf (proto-parent model) message)
                 (setf (proto-messages message) (nconc (proto-messages message) (list model)))
           (otherwise
            (multiple-value-bind (field slot idx)
                (process-field field index :conc-name conc-name :alias-for alias-for)
-             (assert (not (find (proto-index field) (proto-fields message) :key #'proto-index)) ()
+             (assert (not (find-field message (proto-index field))) ()
                      "The field ~S overlaps with another field in ~S"
                      (proto-value field) (proto-class message))
              (setq index idx)
         ;; If we've got an alias, define a a type that is the subtype of
         ;; the Lisp class that typep and subtypep work
         (unless (or (eq type alias-for) (find-class type nil))
-          (collect-form `(deftype ,type () ',alias-for)))
+          (collect-type-form `(deftype ,type () ',alias-for)))
         ;; If no alias, define the class now
-        (collect-form `(defclass ,type () (,@slots)
+        (collect-type-form `(defclass ,type () (,@slots)
                          ,@(and documentation `((:documentation ,documentation))))))
       `(progn
          define-group
          ,message
-         ,forms
+         ((with-proto-source-location (,type ,name protobuf-message ,@source-location)
+            ,@type-forms
+            ,@forms))
          ,mfield
          ,mslot))))
 
    Returns a 'proto-field' object, a CLOS slot form and the incremented field index."
   (when (i= index 18999)                                ;skip over the restricted range
     (setq index 19999))
-  (destructuring-bind (slot &key type (default nil default-p) reader writer name documentation) field
-    (let* ((idx  (if (listp slot) (second slot) (iincf index)))
+  (destructuring-bind (slot &rest other-options 
+                       &key type reader writer name (default nil default-p) packed
+                            ((:index idx)) options documentation &allow-other-keys) field
+    ;; Allow old ((slot index) ...) or new (slot :index ...),
+    ;; but only allow one of those two to be used simultaneously
+    (assert (if idx (not (listp slot)) t) ()
+            "Use either ((slot index) ...)  or (slot :index index ...), but not both")
+    (let* ((idx  (or idx (if (listp slot) (second slot) (iincf index))))
            (slot (if (listp slot) (first slot) slot))
-           (reqd (clos-type-to-protobuf-required type))
-           (reader (if (eq reader 't)
-                     (intern (if conc-name (format nil "~A~A" conc-name slot) (symbol-name slot))
-                             (symbol-package slot))
-                     reader)))
+           (reader (or reader
+                       (and conc-name
+                            (intern (format nil "~A~A" conc-name slot) *protobuf-package*))))
+           (options (append
+                     (loop for (key val) on other-options by #'cddr
+                           unless (member key '(:type :reader :writer :name :default :packed :documentation))
+                             collect (make-instance 'protobuf-option
+                                       :name  (slot-name->proto key)
+                                       :value val))
+                     (loop for (key val) on options by #'cddr
+                         collect (make-instance 'protobuf-option
+                                   :name  (if (symbolp key) (slot-name->proto key) key)
+                                   :value val)))))
       (multiple-value-bind (ptype pclass)
           (clos-type-to-protobuf-type type)
-        (let ((slot (unless alias-for
-                      `(,slot :type ,type
-                              ,@(and reader
-                                     (if writer
-                                       `(:reader ,reader)
-                                       `(:accessor ,reader)))
-                              ,@(and writer
-                                     `(:writer ,writer))
-                              :initarg ,(kintern (symbol-name slot))
-                              ,@(cond ((and (not default-p) 
-                                            (eq reqd :repeated))
-                                       `(:initform ()))
-                                      ((and (not default-p)
-                                            (eq reqd :optional)
-                                            ;; Use unbound for booleans only
-                                            (not (eq pclass :bool)))
-                                       `(:initform nil))
-                                      (default-p
-                                        `(:initform ,(protobuf-default-to-clos-init default type)))))))
-              (field (make-instance 'protobuf-field
-                       :name  (or name (slot-name->proto slot))
-                       :type  ptype
-                       :class pclass
-                       :required reqd
-                       :index  idx
-                       :value  slot
-                       :reader reader
-                       :writer writer
-                       :default default
-                       :packed  (and (eq reqd :repeated)
-                                     (packed-type-p pclass))
-                       :documentation documentation)))
-          (values field slot idx))))))
+        (multiple-value-bind (reqd vectorp)
+            (clos-type-to-protobuf-required type)
+          (let* ((default (if (eq reqd :repeated)
+                            (if vectorp $empty-vector $empty-list)      ;to distinguish between list-of and vector-of
+                            (if default-p default $empty-default)))
+                 (cslot (unless alias-for
+                          `(,slot :type ,type
+                                  ,@(and reader
+                                         (if writer
+                                           `(:reader ,reader)
+                                           `(:accessor ,reader)))
+                                  ,@(and writer
+                                         `(:writer ,writer))
+                                  :initarg ,(kintern (symbol-name slot))
+                                  ,@(cond ((eq reqd :repeated)
+                                           ;; Repeated fields get a container for their elements
+                                           (if vectorp
+                                             `(:initform (make-array 5 :fill-pointer 0 :adjustable t))
+                                             `(:initform ())))
+                                          ((and (not default-p)
+                                                (eq reqd :optional)
+                                                ;; Use unbound for booleans only
+                                                (not (eq pclass :bool)))
+                                           `(:initform nil))
+                                          (default-p
+                                            `(:initform ,(protobuf-default-to-clos-init default type)))))))
+                 (field (make-instance 'protobuf-field
+                          :name  (or name (slot-name->proto slot))
+                          :type  ptype
+                          :class pclass
+                          :qualified-name (make-qualified-name *protobuf* (or name (slot-name->proto slot)))
+                          :parent *protobuf*
+                          ;; One of :required, :optional or :repeated
+                          :required reqd
+                          :index  idx
+                          :value  slot
+                          :reader reader
+                          :writer writer
+                          :default default
+                          ;; Pack the field only if requested and it actually makes sense
+                          :packed  (and (eq reqd :repeated) packed t)
+                          :options options
+                          :documentation documentation)))
+            (values field cslot idx)))))))
+
+(defparameter *rpc-package* nil
+  "The Lisp package that implements RPC.
+   This should be set when an RPC package that uses CL-Protobufs gets loaded.")
+(defparameter *rpc-call-function* nil
+  "The Lisp function that implements RPC client-side calls.
+   This should be set when an RPC package that uses CL-Protobufs gets loaded.")
 
 ;; Define a service named 'type' with generic functions declared for
 ;; each of the methods within the service
-(defmacro define-service (type (&key name options documentation)
+(defmacro define-service (type (&key name options
+                                     documentation source-location)
                           &body method-specs)
   "Define a service named 'type' and Lisp 'defgeneric' for all its methods.
    'name' can be used to override the defaultly generated Protobufs service name.
    'options' is a set of keyword/value pairs, both of which are strings.
 
-   The body is a set of method specs of the form (name (input-type output-type) &key options).
+   The body is a set of method specs of the form (name (input-type [=>] output-type) &key options).
    'input-type' and 'output-type' may also be of the form (type &key name)."
   (let* ((name    (or name (class-name->proto type)))
          (options (loop for (key val) on options by #'cddr
                         collect (make-instance 'protobuf-option
-                                  :name  key
+                                  :name  (if (symbolp key) (slot-name->proto key) key)
                                   :value val)))
          (service (make-instance 'protobuf-service
                     :class type
                     :name  name
+                    :qualified-name (make-qualified-name *protobuf* name)
+                    :parent *protobuf*
                     :options options
-                    :documentation documentation)))
+                    :documentation documentation
+                    :source-location source-location))
+         (index 0))
     (with-collectors ((forms collect-form))
       (dolist (method method-specs)
-        (destructuring-bind (function (input-type output-type) &key name options documentation) method
-          (let* ((input-name (and (listp input-type)
+        (destructuring-bind (function (&rest types)
+                             &key name options documentation source-location) method
+          (let* ((input-type   (first types))
+                 (output-type  (if (string= (string (second types)) "=>") (third types) (second types)))
+                 (streams-type (if (string= (string (second types)) "=>")
+                                 (getf (cdddr types) :streams)
+                                 (getf (cddr  types) :streams)))
+                 (input-name (and (listp input-type)
                                   (getf (cdr input-type) :name)))
                  (input-type (if (listp input-type) (car input-type) input-type))
                  (output-name (and (listp output-type)
                                    (getf (cdr output-type) :name)))
                  (output-type (if (listp output-type) (car output-type) output-type))
+                 (streams-name (and (listp streams-type)
+                                    (getf (cdr streams-type) :name)))
+                 (streams-type (if (listp streams-type) (car streams-type) streams-type))
                  (options (loop for (key val) on options by #'cddr
                                 collect (make-instance 'protobuf-option
-                                          :name  key
+                                          :name  (if (symbolp key) (slot-name->proto key) key)
                                           :value val)))
+                 (package   *protobuf-package*)
+                 (client-fn function)
+                 (server-fn (intern (format nil "~A-~A" 'do function) package))
                  (method  (make-instance 'protobuf-method
                             :class function
                             :name  (or name (class-name->proto function))
+                            :qualified-name (make-qualified-name *protobuf* (or name (class-name->proto function)))
+                            :parent service
+                            :client-stub client-fn
+                            :server-stub server-fn
                             :input-type  input-type
                             :input-name  (or input-name (class-name->proto input-type))
                             :output-type output-type
                             :output-name (or output-name (class-name->proto output-type))
+                            :streams-type streams-type
+                            :streams-name (and streams-type
+                                               (or streams-name (class-name->proto streams-type)))
+                            :index (iincf index)
                             :options options
-                            :documentation documentation)))
+                            :documentation documentation
+                            :source-location source-location)))
             (setf (proto-methods service) (nconc (proto-methods service) (list method)))
-            ;; The following are the hooks to CL-Stubby
-            (let* ((package   (symbol-package function))
-                   (client-fn function)
-                   (server-fn (intern (format nil "~A-~A" 'do function) package))
-                   (vinput    (intern (format nil "~A-~A" (symbol-name input-type) 'in) package))
-                   (voutput   (intern (format nil "~A-~A" (symbol-name output-type) 'out) package))
+            ;; The following are the hooks to an RPC implementation
+            (let* ((vrequest  (intern (symbol-name 'request) package))
                    (vchannel  (intern (symbol-name 'channel) package))
                    (vcallback (intern (symbol-name 'callback) package)))
               ;; The client side stub, e.g., 'read-air-reservation'.
-              ;; The expectation is that CL-Stubby will provide macrology to make it
+              ;; The expectation is that the RPC implementation will provide code to make it
               ;; easy to implement a method for this on each kind of channel (HTTP, TCP socket,
               ;; IPC, etc). Unlike C++/Java/Python, we don't need a client-side subclass,
               ;; because we can just use multi-methods.
-              ;; The CL-Stubby macros take care of serializing the input, transmitting the
+              ;; The 'do-XXX' method calls the RPC code with the channel, the method
+              ;; (i.e., a 'protobuf-method' object), the request and the callback function.
+              ;; The RPC code should take care of serializing the input, transmitting the
               ;; request over the wire, waiting for input (or not if it's asynchronous),
-              ;; filling in the output, and calling the callback (if it's synchronous).
-              ;; It's not very Lispy to side-effect an output object, but it makes
-              ;; asynchronous calls simpler.
-              (collect-form `(defgeneric ,client-fn (,vchannel ,vinput ,voutput &key ,vcallback)
+              ;; filling in the output, and either returning the response (if synchronous)
+              ;; or calling the callback with the response as an argument (if asynchronous).
+              ;; It will also deserialize the response so that the client code sees the
+              ;; response as an application object.
+              (collect-form `(defgeneric ,client-fn (,vchannel ,vrequest &key ,vcallback)
                                ,@(and documentation `((:documentation ,documentation)))
-                               (declare (values ,output-type))))
+                               #-sbcl (declare (values ,output-type))
+                               (:method (,vchannel (,vrequest ,input-type) &key ,vcallback)
+                                 (declare (ignorable ,vchannel ,vcallback))
+                                 (let ((call (and *rpc-package* *rpc-call-function*)))
+                                   (assert call ()
+                                           "There is no RPC package loaded!")
+                                   (funcall call ,vchannel ',method ,vrequest
+                                            :callback ,vcallback)))))
               ;; The server side stub, e.g., 'do-read-air-reservation'.
               ;; The expectation is that the server-side program will implement
               ;; a method with the business logic for this on each kind of channel
               ;; (HTTP, TCP socket, IPC, etc), possibly on a server-side subclass
-              ;; of the input class
+              ;; of the input class.
               ;; The business logic is expected to perform the correct operations on
               ;; the input object, which arrived via Protobufs, and produce an output
-              ;; of the given type, which will be serialized as a result.
+              ;; of the given type, which will be serialized and sent back over the wire.
               ;; The channel objects hold client identity information, deadline info,
-              ;; etc, and can be side-effected to indicate success or failure
-              ;; CL-Stubby provides the channel classes and does (de)serialization, etc
-              (collect-form `(defgeneric ,server-fn (,vchannel ,vinput ,voutput &key ,vcallback)
+              ;; etc, and can be side-effected to indicate success or failure.
+              ;; The RPC code provides the channel classes and does (de)serialization, etc
+              (collect-form `(defgeneric ,server-fn (,vchannel ,vrequest)
                                ,@(and documentation `((:documentation ,documentation)))
-                               (declare (values ,output-type))))))))
+                               #-sbcl (declare (values ,output-type))))))))
       `(progn
          define-service
          ,service
-         ,forms))))
+         ((with-proto-source-location (,type ,name protobuf-service ,@source-location)
+            ,@forms))))))
+
+
+;; Lisp-only type aliases
+(defmacro define-type-alias (type (&key name alias-for documentation source-location)
+                             &key lisp-type proto-type serializer deserializer)
+  "Define a Protobufs type alias Lisp 'deftype' named 'type'.
+   'lisp-type' is the name of the Lisp type.
+   'proto-type' is the name of a primitive Protobufs type, e.g., 'int32' or 'string'.
+   'serializer' is a function that takes a Lisp object and generates a Protobufs object.
+   'deserializer' is a function that takes a Protobufs object and generates a Lisp object.
+   If 'alias-for' is given, no Lisp 'deftype' will be defined."
+  (multiple-value-bind (type-str proto)
+      (lisp-type-to-protobuf-type proto-type)
+    (assert (keywordp proto) ()
+            "The alias ~S must resolve to a Protobufs primitive type"
+            type)
+    (let* ((name  (or name (class-name->proto type)))
+           (alias (make-instance 'protobuf-type-alias
+                    :class  type
+                    :name   name
+                    :lisp-type  lisp-type
+                    :proto-type proto
+                    :proto-type-str type-str
+                    :serializer   serializer
+                    :deserializer deserializer
+                    :qualified-name (make-qualified-name *protobuf* name)
+                    :parent *protobuf*
+                    :documentation documentation
+                    :source-location source-location)))
+      (with-collectors ((forms collect-form))
+        (if alias-for
+            ;; If we've got an alias, define a a type that is the subtype of
+            ;; the Lisp enum so that typep and subtypep work
+            (unless (eq type alias-for)
+              (collect-form `(deftype ,type () ',alias-for)))
+            ;; If no alias, define the Lisp enum type now
+            (collect-form `(deftype ,type () ',lisp-type)))
+        `(progn
+           define-type-alias
+           ,alias
+           ((with-proto-source-location (,type ,name protobuf-type-alias ,@source-location)
+              ,@forms)))))))
 
 \f
 ;;; Ensure everything in a Protobufs schema is defined
 (defvar *undefined-messages*)
 
 ;; A very useful tool during development...
-(defun ensure-all-protobufs ()
+(defun ensure-all-schemas ()
   (let ((protos (sort
                  (delete-duplicates
-                  (loop for p being the hash-values of *all-protobufs*
+                  (loop for p being the hash-values of *all-schemas*
                         collect p))
                  #'string< :key #'proto-name)))
-    (mapcan #'ensure-protobuf protos)))
-
-(defmethod ensure-protobuf ((proto protobuf))
-  "Ensure that all of the types are defined in the Protobufs schema 'proto'.
-   This returns two values:
-    - A list whose elements are (<undefined-type> \"message:field\" ...)
-    - The accumulated warnings table that has the same information as objects."
-  (let ((*undefined-messages* (make-hash-table))
-        (trace (list proto)))
-    (map () (curry #'ensure-message trace) (proto-messages proto))
-    (map () (curry #'ensure-service trace) (proto-services proto))
-    (loop for type being the hash-keys of *undefined-messages*
-            using (hash-value things)
-          collect (list* type
-                         (mapcar #'(lambda (thing)
-                                     (format nil "~A:~A" (proto-name (car thing)) (proto-name (cdr thing))))
-                                 things)) into warnings
-          finally (return (values warnings *undefined-messages*)))))
-
-(defmethod ensure-message (trace (message protobuf-message))
-  (let ((trace (cons message trace)))
-    (map () (curry #'ensure-message trace) (proto-messages message))
-    (map () (curry #'ensure-field trace message) (proto-fields message))))
-
-(defmethod ensure-field (trace message (field protobuf-field))
-  (ensure-type trace message field (proto-class field)))
-
-(defmethod ensure-service (trace (service protobuf-service))
-  (map () (curry #'ensure-method trace service) (proto-methods service)))
-
-(defmethod ensure-method (trace service (method protobuf-method))
-  (ensure-type trace service method (proto-input-type method))
-  (ensure-type trace service method (proto-output-type method)))
+    (mapcan #'ensure-schema protos)))
+
+(defgeneric ensure-schema (schema)
+  (:documentation
+   "Ensure that all of the types are defined in the Protobufs schema 'schema'.
+    This returns two values:
+     - A list whose elements are (<undefined-type> \"message:field\" ...)
+     - The accumulated warnings table that has the same information as objects.")
+  (:method ((schema protobuf-schema))
+    (let ((*undefined-messages* (make-hash-table))
+          (trace (list schema)))
+      (map () (curry #'ensure-message trace) (proto-messages schema))
+      (map () (curry #'ensure-service trace) (proto-services schema))
+      (loop for type being the hash-keys of *undefined-messages*
+              using (hash-value things)
+            collect (list* type
+                           (mapcar #'(lambda (thing)
+                                       (format nil "~A:~A" (proto-name (car thing)) (proto-name (cdr thing))))
+                                   things)) into warnings
+            finally (return (values warnings *undefined-messages*))))))
+
+(defgeneric ensure-message (trace message)
+  (:method (trace (message protobuf-message))
+    (let ((trace (cons message trace)))
+      (map () (curry #'ensure-message trace) (proto-messages message))
+      (map () (curry #'ensure-field trace message) (proto-fields message)))))
+
+(defgeneric ensure-field (trace message field)
+  (:method (trace message (field protobuf-field))
+    (ensure-type trace message field (proto-class field))))
+
+(defgeneric ensure-service (trace service)
+  (:method (trace (service protobuf-service))
+    (map () (curry #'ensure-method trace service) (proto-methods service))))
+
+(defgeneric ensure-method (trace service method)
+  (:method (trace service (method protobuf-method))
+    (ensure-type trace service method (proto-input-type method))
+    (ensure-type trace service method (proto-output-type method))
+    (ensure-type trace service method (proto-streams-type method))))
 
 ;; 'message' and 'field' can be a message and a field or a service and a method
 (defun ensure-type (trace message field type)