Squashed 'third_party/allwpilib_2019/' content from commit bd05dfa1c

Change-Id: I2b1c2250cdb9b055133780c33593292098c375b7
git-subtree-dir: third_party/allwpilib_2019
git-subtree-split: bd05dfa1c7cca74c4fac451e7b9d6a37e7b53447
diff --git a/ntcore/.styleguide b/ntcore/.styleguide
new file mode 100644
index 0000000..4c00fa9
--- /dev/null
+++ b/ntcore/.styleguide
@@ -0,0 +1,31 @@
+cHeaderFileInclude {
+  _c\.h$
+}
+
+cppHeaderFileInclude {
+  (?<!_c)\.h$
+  \.inc$
+}
+
+cppSrcFileInclude {
+  \.cpp$
+}
+
+generatedFileExclude {
+  ntcore/doc/
+}
+
+repoRootNameOverride {
+  ntcore
+}
+
+includeGuardRoots {
+  ntcore/src/main/native/cpp/
+  ntcore/src/main/native/include/
+  ntcore/src/test/native/cpp/
+}
+
+includeOtherLibs {
+  ^support/
+  ^wpi/
+}
diff --git a/ntcore/CMakeLists.txt b/ntcore/CMakeLists.txt
new file mode 100644
index 0000000..47e2264
--- /dev/null
+++ b/ntcore/CMakeLists.txt
@@ -0,0 +1,72 @@
+project(ntcore)
+
+file(GLOB
+    ntcore_native_src src/main/native/cpp/*.cpp
+    ntcore_native_src src/main/native/cpp/networktables/*.cpp
+    ntcore_native_src src/main/native/cpp/tables/*.cpp)
+add_library(ntcore ${ntcore_native_src})
+set_target_properties(ntcore PROPERTIES DEBUG_POSTFIX "d")
+target_include_directories(ntcore PUBLIC
+                $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/include>
+                            $<INSTALL_INTERFACE:${include_dest}/ntcore>)
+target_link_libraries(ntcore PUBLIC wpiutil)
+
+set_property(TARGET ntcore PROPERTY FOLDER "libraries")
+
+install(TARGETS ntcore EXPORT ntcore DESTINATION "${main_lib_dest}")
+install(DIRECTORY src/main/native/include/ DESTINATION "${include_dest}/ntcore")
+
+if (MSVC)
+    set (ntcore_config_dir ${wpilib_dest})
+else()
+    set (ntcore_config_dir share/ntcore)
+endif()
+
+install(FILES ntcore-config.cmake DESTINATION ${ntcore_config_dir})
+install(EXPORT ntcore DESTINATION ${ntcore_config_dir})
+
+# Java bindings
+if (NOT WITHOUT_JAVA)
+    find_package(Java REQUIRED)
+    find_package(JNI REQUIRED)
+    include(UseJava)
+    set(CMAKE_JAVA_COMPILE_FLAGS "-Xlint:unchecked")
+
+    file(GLOB
+        ntcore_jni_src src/main/native/cpp/jni/NetworkTablesJNI.cpp)
+
+    file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java)
+    set(CMAKE_JNI_TARGET true)
+
+    if(${CMAKE_VERSION} VERSION_LESS "3.11.0")
+        set(CMAKE_JAVA_COMPILE_FLAGS "-h" "${CMAKE_CURRENT_BINARY_DIR}/jniheaders")
+        add_jar(ntcore_jar ${JAVA_SOURCES} INCLUDE_JARS wpiutil_jar OUTPUT_NAME ntcore)
+    else()
+        add_jar(ntcore_jar ${JAVA_SOURCES} INCLUDE_JARS wpiutil_jar OUTPUT_NAME ntcore GENERATE_NATIVE_HEADERS ntcore_jni_headers)
+    endif()
+
+    get_property(NTCORE_JAR_FILE TARGET ntcore_jar PROPERTY JAR_FILE)
+    install(FILES ${NTCORE_JAR_FILE} DESTINATION "${java_lib_dest}")
+
+    set_property(TARGET ntcore_jar PROPERTY FOLDER "java")
+
+    add_library(ntcorejni ${ntcore_jni_src})
+    target_link_libraries(ntcorejni PUBLIC ntcore wpiutil)
+
+    set_property(TARGET ntcorejni PROPERTY FOLDER "libraries")
+
+    if (MSVC)
+        install(TARGETS ntcorejni RUNTIME DESTINATION "${jni_lib_dest}" COMPONENT Runtime)
+    endif()
+
+    if(${CMAKE_VERSION} VERSION_LESS "3.11.0")
+        target_include_directories(ntcorejni PRIVATE ${JNI_INCLUDE_DIRS})
+        target_include_directories(ntcorejni PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/jniheaders")
+    else()
+        target_link_libraries(ntcorejni PRIVATE ntcore_jni_headers)
+    endif()
+    add_dependencies(ntcorejni ntcore_jar)
+
+    install(TARGETS ntcorejni EXPORT ntcorejni DESTINATION "${main_lib_dest}")
+
+endif()
diff --git a/ntcore/build.gradle b/ntcore/build.gradle
new file mode 100644
index 0000000..659b0f9
--- /dev/null
+++ b/ntcore/build.gradle
@@ -0,0 +1,49 @@
+ext {
+    nativeName = 'ntcore'
+    devMain = 'edu.wpi.first.ntcore.DevMain'
+}
+
+apply from: "${rootDir}/shared/jni/setupBuild.gradle"
+
+model {
+    // Exports config is a utility to enable exporting all symbols in a C++ library on windows to a DLL.
+    // This removes the need for DllExport on a library. However, the gradle C++ builder has a bug
+    // where some extra symbols are added that cannot be resolved at link time. This configuration
+    // lets you specify specific symbols to exlude from exporting.
+    exportsConfigs {
+        ntcore(ExportsConfig) {
+            x86ExcludeSymbols = ['_CT??_R0?AV_System_error', '_CT??_R0?AVexception', '_CT??_R0?AVfailure',
+                                 '_CT??_R0?AVbad_cast',
+                                 '_CT??_R0?AVruntime_error', '_CT??_R0?AVsystem_error', '_CTA5?AVfailure',
+                                 '_TI5?AVfailure']
+            x64ExcludeSymbols = ['_CT??_R0?AV_System_error', '_CT??_R0?AVexception', '_CT??_R0?AVfailure',
+                                 '_CT??_R0?AVbad_cast',
+                                 '_CT??_R0?AVruntime_error', '_CT??_R0?AVsystem_error', '_CTA5?AVfailure',
+                                 '_TI5?AVfailure']
+        }
+        ntcoreJNI(ExportsConfig) {
+            x86SymbolFilter = { symbols ->
+                def retList = []
+                symbols.each { symbol ->
+                    if (symbol.startsWith('NT_')) {
+                        retList << symbol
+                    }
+                }
+                return retList
+            }
+            x64SymbolFilter = { symbols ->
+                def retList = []
+                symbols.each { symbol ->
+                    if (symbol.startsWith('NT_')) {
+                        retList << symbol
+                    }
+                }
+                return retList
+            }
+        }
+    }
+}
+
+pmdMain {
+    pmdMain.enabled = false
+}
diff --git a/ntcore/doc/alloy-model.adoc b/ntcore/doc/alloy-model.adoc
new file mode 100644
index 0000000..35ca7d8
--- /dev/null
+++ b/ntcore/doc/alloy-model.adoc
@@ -0,0 +1,198 @@
+= Network Tables Alloy Model
+
+Alloy (http://alloy.mit.edu/alloy/) is a formal logic tool that can analyze
+first-order logic expressions. Under the proposed sequence number -based
+protocol, assuming that all nodes start from the same state, Alloy is unable to
+find a way where two nodes with the same sequence number have different state
+when activity ceases.
+
+The model used is included below. Although Alloy cannot test all cases, since
+such an exhaustive search is intractable, it provides a high level of
+confidence in the proposed protocol.
+
+----
+--- Models a distributed, centralized hash table system called NetworkTables
+--- System state is protected by sequence numbers; the server's value for a certain sequence number always wins
+--- Paul Malmsten, 2012 pmalmsten@gmail.com
+
+open util/ordering[Time] as TO
+open util/natural as natural
+
+sig Time {}
+sig State {}
+
+--- Define nodes and server
+sig Node {
+    state: State -> Time,
+    sequenceNumber: Natural -> Time
+}
+
+--- Only one server
+one sig Server extends Node {
+}
+
+--- Define possible events
+abstract sig Event {
+    pre,post: Time,
+    receiver: one Node
+}
+
+// For all events, event.post is the time directly following event.pre
+fact {
+    all e:Event {
+        e.post = e.pre.next
+    }
+}
+
+// Represents that state has changed on a node
+sig StateChangeEvent extends Event {
+}
+
+// Represents that state has been transferred from one node to another
+sig StateTransferEvent extends Event {
+    sender: one Node
+}
+
+fact {
+    --- Every node must assume at most one state
+    all t:Time, n:Node | #n.state.t = 1
+
+    --- Every node must assume one sequence number
+    all t:Time, n:Node | #n.sequenceNumber.t = 1
+
+    --- Sequence numbers may only increment
+    all t:Time - last, n:Node | let t' = t.next | natural/gte[n.sequenceNumber.t', n.sequenceNumber.t]
+}
+
+
+fact stateChangedImpliesAStateTransfer {
+    all sce:StateChangeEvent {
+        // A StateChange on a client causes a transfer to the Server if its sequence number is greater than the server's
+        sce.receiver in Node - Server and natural/gt[sce.receiver.sequenceNumber.(sce.post), Server.sequenceNumber.(sce.post)]
+         implies
+            some ste:StateTransferEvent {
+                ste.pre = sce.post and ste.sender = sce.receiver and ste.receiver = Server
+            }
+    }
+
+    all sce:StateChangeEvent {
+        // A StateChange on the server causes a transfer to all clients
+        sce.receiver = Server implies
+            all n:Node - Server {
+                some ste:StateTransferEvent {
+                     ste.pre = sce.post and ste.sender = Server and ste.receiver = n
+                }
+            }
+    }
+
+    all sce:StateTransferEvent {
+        // A StateTransfer to the server causes a transfer to all clients
+        sce.receiver = Server implies
+            all n:Node - Server {
+                some ste:StateTransferEvent {
+                     ste.pre = sce.post and ste.sender = Server and ste.receiver = n
+                }
+            }
+    }
+}
+
+fact stateTransferEventsMoveState {
+    all ste:StateTransferEvent {
+        ste.sender = Server and not ste.receiver = Server or ste.receiver = Server and not ste.sender = Server
+
+        // Nodes can only post to the server if their sequence number is greater than the servers
+        ste.receiver = Server implies natural/gt[ste.sender.sequenceNumber.(ste.pre), ste.receiver.sequenceNumber.(ste.pre)]
+
+        // Server can only post to clients if its sequence number is greater than or equal to the client
+        ste.sender = Server implies natural/gte[ste.sender.sequenceNumber.(ste.pre), ste.receiver.sequenceNumber.(ste.pre)]
+
+        // Actual transfer
+        (ste.receiver.state.(ste.post) = ste.sender.state.(ste.pre) and
+          ste.receiver.sequenceNumber.(ste.post) = ste.sender.sequenceNumber.(ste.pre))
+    }
+}
+
+
+fact noEventsPendingAtEnd {
+    no e:Event {
+        e.pre = last
+    }
+}
+
+fact noDuplicateEvents {
+    all e,e2:Event {
+        // Two different events with the same receiver imply they occurred at different times
+        e.receiver = e2.receiver and e != e2 implies e.pre != e2.pre
+    }
+}
+
+fact noStateTransfersToSelf {
+    all ste:StateTransferEvent {
+        ste.sender != ste.receiver
+    }
+}
+
+fact noDuplicateStateTransferEvents {
+    all ste,ste2:StateTransferEvent {
+        // Two state transfer events with the same nodes imply that they occurred at different times
+        ste.sender = ste2.sender and ste.receiver = ste2.receiver and ste != ste2 implies ste.pre != ste2.pre
+    }
+}
+
+--- Trace (time invariant)
+fact trace {
+    all t:Time - last | let t' = t.next {
+        all n:Node {
+                // A node in (pre.t).receiver means it is being pointed to by some event that will happen over the next time step
+                n in (pre.t).receiver implies n.state.t' != n.state.t and n.sequenceNumber.t' != n.sequenceNumber.t    // A node which receives some sort of event at time t causes it to change state
+                                                       else n.state.t' = n.state.t and n.sequenceNumber.t' = n.sequenceNumber.t     // Otherwise, it does not change state
+        }
+    }
+}
+
+--- Things we might like to be true, but are not always true
+
+pred atLeastOneEvent {
+    #Event >= 1
+}
+
+pred allNodesStartAtSameStateAndSequenceNumber {
+    all n,n2:Node {
+        n.state.first = n2.state.first and n.sequenceNumber.first = n2.sequenceNumber.first
+    }
+}
+
+pred noStateChangeEventsAtEnd {
+    no e:StateChangeEvent {
+        e.post = last
+    }
+}
+
+--- Demonstration (Alloy will try to satisfy this)
+
+pred show {
+    atLeastOneEvent
+}
+run show
+
+--- Assertions (Alloy will try to break these)
+
+assert allNodesConsistentAtEnd {
+    allNodesStartAtSameStateAndSequenceNumber implies
+        all n,n2:Node {
+            // At the end of a sequence (last) all nodes with the same sequence number have the same state
+            n.sequenceNumber.last = n2.sequenceNumber.last implies n.state.last = n2.state.last
+        }
+}
+check allNodesConsistentAtEnd for 3 Event, 10 Node, 3 State, 5 Time, 5 Natural
+check allNodesConsistentAtEnd for 8 Event, 2 Node, 5 State, 9 Time, 9 Natural
+
+assert serverHasHighestSeqNumAtEnd {
+    allNodesStartAtSameStateAndSequenceNumber implies
+        all n:Node - Server{
+            // At the end of a sequence (last) all nodes with the same sequence number have the same state
+            natural/gte[Server.sequenceNumber.last, n.sequenceNumber.last]
+        }
+}
+check serverHasHighestSeqNumAtEnd for 3 Event, 10 Node, 3 State, 5 Time, 5 Natural
+----
diff --git a/ntcore/doc/networktables2.adoc b/ntcore/doc/networktables2.adoc
new file mode 100644
index 0000000..8471f86
--- /dev/null
+++ b/ntcore/doc/networktables2.adoc
@@ -0,0 +1,456 @@
+= Network Tables Protocol Specification, Version 2.0
+WPILib Developers <wpilib@wpi.edu>
+Protocol Revision 2.0 (0x0200), 1/8/2013
+:toc:
+:toc-placement: preamble
+:sectanchors:
+
+This document defines a network protocol for a key-value store that may be read
+from and written to by multiple remote clients. A central server, most often
+running on a FIRST FRC robot controller, is responsible for providing
+information consistency and for facilitating communication between clients.
+This document describes protocol revision 2.0 (0x0200).
+
+Information consistency is guaranteed through the use of a sequence number
+associated with each key-value pair. An update of a key-value pair increments
+the associated sequence number, and this update information is shared with all
+participating clients. The central server only applies and redistributes
+updates which have a larger sequence number than its own, which guarantees that
+a client must have received a server's most recent state before it can replace
+it with a new value.
+
+This is a backwards-incompatible rework of the Network Tables network protocol
+originally introduced for the 2012 FIRST Robotics Competition. Note that this
+revision of the Network Tables protocol no longer includes the concept of
+sub-tables. We suggest that instead of representing sub-tables as first-class
+data types in the network protocol, it would be easy for an implementation to
+provide a similar API abstraction by adding prefixes to keys. For example, we
+suggest using Unix-style path strings to define sub-table hierarchies. The
+prefix ensures that sub-table namespaces do not collide in a global hashtable
+without requiring an explicit sub-table representation.
+
+In addition, the explicit concept of grouping multiple updates such that they
+are all visible at the same time to user code on a remote device was discarded.
+Instead, array types for all common elements are provided. By using an array
+data type, users may achieve the same level of atomicity for common operations
+(e.g. sending a cartesian coordinate pair) without requiring the complexity of
+arbitrarily grouped updates.
+
+This document conforms to <<rfc2119>> - Key words for use in RFCs to Indicate
+Requirement Levels.
+
+[[references]]
+== References
+
+[[rfc1982,RFC 1982]]
+* RFC 1982, Serial Number Arithmetic, http://tools.ietf.org/html/rfc1982
+
+[[rfc2119,RFC 2119]]
+* RFC 2119, Key words for use in RFCs to Indicate Requirement Levels,
+http://tools.ietf.org/html/rfc2119
+
+[[definitions]]
+== Definitions
+
+[[def-client]]
+Client:: An implementation of this protocol running in client configuration.
+Any number of Clients may exist for a given Network.
+
+[[def-entry]]
+Entry:: A data value identified by a string name.
+
+[[def-entry-id]]
+Entry ID:: An unsigned 2-byte ID by which the Server and Clients refer to an
+Entry across the network instead of using the full string key for the Entry.
+Entry IDs range from 0x0000 to 0xFFFE (0xFFFF is reserved for an Entry
+Assignment issued by a Client).
+
+[[def-server]]
+Server:: An implementation of this protocol running in server configuration.
+One and only one Server must exist for a given Network.
+
+[[def-network]]
+Network:: One or more Client nodes connected to a Server.
+
+[[def-user-code]]
+User Code:: User-supplied code which may interact with a Client or Server. User
+Code should be executed on the same computer as the Client or Server instance
+it interacts with.
+
+[[def-sequence-number]]
+Sequence Number:: An unsigned number which allows the Server to resolve update
+conflicts between Clients and/or the Server. Sequence numbers may overflow.
+Sequential arithmetic comparisons, which must be used with Sequence Numbers,
+are defined by RFC 1982.
+
+[[def-protocol-revision]]
+Protocol Revision:: A 16-bit unsigned integer which indicates the version of
+the network tables protocol that a client wishes to use. The protocol revision
+assigned to this version of the network tables specification is listed at the
+top of this document. This number is listed in dot-decimal notation as well as
+its equivalent hexadecimal value.
+
+== Transport Layer
+
+Conventional implementations of this protocol should use TCP for reliable
+communication; the Server should listen on TCP port 1735 for incoming
+connections.
+
+== Example Exchanges
+
+[[exchange-connect]]
+=== Client Connects to the Server
+
+Directly after client establishes a connection with the Server, the following
+procedure must be followed:
+
+. The Client sends a <<msg-client-hello>> message to the Server
+
+. The Server sends one <<msg-assign>> for every field it currently recognizes.
+
+. The Server sends a <<msg-server-hello-complete>> message.
+
+. For all Entries the Client recognizes that the Server did not identify with a
+Entry Assignment, the client follows the <<exchange-client-creates-entry>>
+protocol.
+
+In the event that the Server does not support the protocol revision that the
+Client has requested in a Client Hello message, the Server must instead issue a
+<<msg-protocol-unsupported>> message to the joining client and close the
+connection.
+
+[[exchange-client-creates-entry]]
+=== Client Creates an Entry
+
+When User Code on a Client assigns a value to an Entry that the Server has not
+yet issued a Entry Assignment for, the following procedure must be followed:
+
+. The Client sends an <<msg-assign>> with an Entry ID of 0xFFFF.
+
+. The Server issues an <<msg-assign>> to all Clients (including the sender) for
+the new field containing a real Entry ID and Sequence Number for the new field.
+
+In the event that User Code on the Client updates the value of the
+to-be-announced field again before the expected Entry Assignment is received,
+then the Client must save the new value and take no other action (the most
+recent value of the field should be issued when the Entry Assignment arrives,
+if it differs from the value contained in the received Entry Assignment).
+
+In the event that the Client receives a Entry Assignment from the Server for
+the Entry that it intended to issue an Entry Assignment for, before it issued
+its own Entry Assignment, the procedure may end early.
+
+In the event that the Server receives a duplicate Entry Assignment from a
+Client (likely due to the client having not yet received the Server's Entry
+Assignment), the Server should ignore the duplicate Entry Assignment.
+
+[[exchange-client-updates-entry]]
+=== Client Updates an Entry
+
+When User Code on a Client updates the value of an Entry, the Client must send
+an <<msg-update>> message to the Server. The Sequence Number included in the
+Entry Update message must be the most recently received Sequence Number for the
+Entry to be updated incremented by one.
+
+.Example:
+
+. Client receives Entry Assignment message for Entry "a" with integer value 1,
+Entry ID of 0, and Sequence Number 1.
+
+. User Code on Client updates value of Entry "a" to 16 (arbitrary).
+
+. Client sends Entry Update message to Server for Entry 0 with a Sequence
+Number of 2 and a value of 16.
+
+When the Server receives an Entry Update message, it first checks the Sequence
+Number in the message against the Server's value for the Sequence Number
+associated with the Entry to be updated. If the received Sequence Number is
+strictly greater than (aside: see definition of "greater than" under the
+definition of Sequence Number) the Server's Sequence Number for the Entry to be
+updated, the Server must apply the new value for the indicated Entry and repeat
+the Entry Update message to all other connected Clients.
+
+If the received Sequence Number is less than or equal (see definition of "less
+than or equal" in RFC 1982) to the Server's Sequence Number for the Entry to be
+updated, this implies that the Client which issued the Entry Update message has
+not yet received one or more Entry Update message(s) that the Server recently
+sent to it; therefore, the Server must ignore the received Entry Update
+message. In the event that comparison between two Sequence Numbers is undefined
+(see RFC 1982), then the Server must always win (it ignores the Entry Update
+message under consideration).
+
+[[update-rate]]
+NOTE: If User Code modifies the value of an Entry too quickly, 1) users may not
+see every value appear on remote machines, and 2) the consistency protection
+offered by the Entry's Sequence Number may be lost (by overflowing before
+remote devices hear recent values). It is recommended that implementations
+detect when user code updates an Entry more frequently than once every 5
+milliseconds and print a warning message to the user (and/or offer some other
+means of informing User Code of this condition).
+
+[[exchange-server-creates-entry]]
+=== Server Creates an Entry
+
+When User Code on the Server assigns a value to a Entry which does not exist,
+the Server must issue an <<msg-assign>> message to all connected clients.
+
+[[exchange-server-updates-entry]]
+=== Server Updates an Entry
+
+When User Code on the Server updates the value of an Entry, the Server must
+apply the new value to the Entry immediately, increment the associated Entry's
+Sequence Number, and issue a <<msg-update>> message containing the new value
+and Sequence Number of the associated Entry to all connected Clients.
+
+NOTE: See <<update-rate,Note>> under <<exchange-client-updates-entry>>.
+
+[[exchange-keep-alive]]
+=== Keep Alive
+
+To maintain a connection and prove a socket is still open, a Client or Server
+may issue <<msg-keep-alive>> messages. Clients and the Server should ignore
+incoming Keep Alive messages.
+
+The intent is that by writing a Keep Alive to a socket, a Client forces its
+network layer (TCP) to reevaluate the state of the network connection as it
+attempts to deliver the Keep Alive message. In the event that a connection is
+no longer usable, a Client's network layer should inform the Client that it is
+no longer usable within a few attempts to send a Keep Alive message.
+
+To provide timely connection status information, Clients should send a Keep
+Alive message to the Server after every 1 second period of connection
+inactivity (i.e. no information is being sent to the Server). Clients should
+not send Keep Alive messages more frequently than once every 100 milliseconds.
+
+Since the Server does not require as timely information about the status of a
+connection, it is not required to send Keep Alive messages during a period of
+inactivity.
+
+[[bandwidth]]
+== Bandwidth and Latency Considerations
+
+To reduce unnecessary bandwidth usage, implementations of this protocol should:
+
+* Send an Entry Update if and only if the value of an Entry is changed to a
+value that is different from its prior value.
+
+* Buffer messages and transmit them in groups, when possible, to reduce
+transport protocol overhead.
+
+* Only send the most recent value of an Entry. When User Code updates the value
+of an Entry more than once before the new value is transmitted, only the latest
+value of the Entry should be sent.
+
+It is important to note that these behaviors will increase the latency between
+when a Client or Server updates the value of an Entry and when all Clients
+reflect the new value. The exact behavior of this buffering is left to
+implementations to determine, although the chosen scheme should reflect the
+needs of User Code. Implementations may include a method by which User Code can
+specify the maximum tolerable send latency.
+
+[[entry-types]]
+== Entry Types
+
+Entry Type must assume one the following values:
+
+[cols="1,3"]
+|===
+|Numeric Value |Type
+
+|0x00
+|Boolean
+
+|0x01
+|Double
+
+|0x02
+|String
+
+|0x10
+|Boolean Array
+
+|0x11
+|Double Array
+
+|0x12
+|String Array
+|===
+
+[[entry-values]]
+== Entry Values
+
+Entry Value must assume the following structure as indicated by Entry Type:
+
+[cols="1,3"]
+|===
+|Entry Type |Entry Value Format
+
+|[[entry-value-boolean]]Boolean
+|1 byte, unsigned; True = 0x01, False = 0x00
+
+|[[entry-value-double]]Double
+|8 bytes, IEEE 754 floating-point "double format" bit layout; (big endian)
+
+|[[entry-value-string]]String
+|2 byte, unsigned length prefix (big endian) of the number of raw bytes to
+follow, followed by the string encoded in UTF-8
+
+|[[entry-value-boolean-array]]Boolean Array
+|1 byte, unsigned, number of elements within the array to follow
+
+N bytes - The raw bytes representing each Boolean element contained within the
+array, beginning with the item at index 0 within the array.
+
+|[[entry-value-double-array]]Double Array
+|1 byte, unsigned, number of elements within the array to follow
+
+N bytes - The raw bytes representing each Double element contained within the
+array, beginning with the item at index 0 within the array.
+
+|[[entry-value-string-array]]String Array
+|1 byte, unsigned, number of elements within the array to follow
+
+N bytes - The raw bytes representing each String element contained within the
+array, beginning with the item at index 0 within the array.
+|===
+
+== Message Structures
+
+All messages are of the following format:
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|Message Type
+|1 byte, unsigned
+
+|Message Data
+|N bytes (length determined by Message Type)
+|===
+
+[[msg-keep-alive]]
+=== Keep Alive
+
+Indicates that the remote party is checking the status of a network connection.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x00 - Keep Alive
+|1 byte, unsigned; Message Type
+|===
+
+[[msg-client-hello]]
+=== Client Hello
+
+A Client issues a Client Hello message when first establishing a connection.
+The Client Protocol Revision field specifies the Network Table protocol
+revision that the Client would like to use.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x01 - Client Hello
+|1 byte, unsigned; Message Type
+
+|Client Protocol Revision
+|2 bytes, Unsigned 16-bit integer (big-endian).
+
+See <<def-protocol-revision,Protocol Revision>>
+|===
+
+[[msg-protocol-unsupported]]
+=== Protocol Version Unsupported
+
+A Server issues a Protocol Version Unsupported message to a Client to inform it
+that the requested protocol revision is not supported. It also includes the
+most recent protocol revision which it supports, such that a Client may
+reconnect under a prior protocol revision if able.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x02 - Protocol Version Unsupported
+|1 byte, unsigned; Message Type
+
+|Server Supported Protocol Revision
+|2 bytes, Unsigned 16-bit integer (big-endian).
+
+See <<def-protocol-revision,Protocol Revision>>
+|===
+
+[[msg-server-hello-complete]]
+=== Server Hello Complete
+
+A Server issues a Server Hello Complete message when it has finished informing
+a newly-connected client of all of the fields it currently recognizes.
+Following the receipt of this message, a Client should inform the Server of
+any/all additional fields that it recognizes that the Server did not announce.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x03 - Server Hello Complete
+|1 byte, unsigned; Message Type
+|===
+
+[[msg-assign]]
+=== Entry Assignment
+
+A Entry Assignment message informs the remote party of a new Entry. An Entry
+Assignment's value field must be the most recent value of the field being
+assigned at the time that the Entry Assignment is sent.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x10 - Entry Assignment
+|1 byte, unsigned; Message Type
+
+|Entry Name
+|<<entry-value-string,String>>
+
+|Entry Type
+|1 byte, unsigned; see <<entry-types,Entry Types>>
+
+|Entry ID
+|2 bytes, unsigned
+
+|Entry Sequence Number
+|2 bytes, unsigned
+
+|Entry Value
+|N bytes, length depends on Entry Type
+|===
+
+If the Entry ID is 0xFFFF, then this assignment represents a request from a
+Client to the Server. In this event, the Entry ID field and the Entry Sequence
+Number field must not be stored or relied upon as they otherwise would be.
+
+[[msg-update]]
+=== Entry Update
+
+An Entry Update message informs a remote party of a new value for an Entry.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x11 - Entry Update
+|1 byte, unsigned; Message Type
+
+|Entry ID
+|2 bytes, unsigned
+
+|Entry Sequence Number
+|2 bytes, unsigned
+
+|Entry Value
+|N bytes, length dependent on value type
+|===
diff --git a/ntcore/doc/networktables3.adoc b/ntcore/doc/networktables3.adoc
new file mode 100644
index 0000000..af8b953
--- /dev/null
+++ b/ntcore/doc/networktables3.adoc
@@ -0,0 +1,885 @@
+= Network Tables Protocol Specification, Version 3.0
+WPILib Developers <wpilib@wpi.edu>
+Protocol Revision 3.0 (0x0300), 6/12/2015
+:toc:
+:toc-placement: preamble
+:sectanchors:
+
+This document defines a network protocol for a key-value store that may be read
+from and written to by multiple remote clients. A central server, most often
+running on a FIRST FRC robot controller, is responsible for providing
+information consistency and for facilitating communication between clients.
+This document describes protocol revision 3.0 (0x0300).
+
+Information consistency is guaranteed through the use of a sequence number
+associated with each key-value pair. An update of a key-value pair increments
+the associated sequence number, and this update information is shared with all
+participating clients. The central server only applies and redistributes
+updates which have a larger sequence number than its own, which guarantees that
+a client must have received a server's most recent state before it can replace
+it with a new value.
+
+This is a backwards-compatible update of <<networktables2,version 2.0>> of the
+Network Tables network protocol. The protocol is designed such that 3.0 clients
+and servers can interoperate with 2.0 clients and servers with the only loss of
+functionality being the extended features introduced in 3.0.
+
+This document conforms to <<rfc2119>> - Key words for use in RFCs to Indicate
+Requirement Levels.
+
+== Summary of Changes from 2.0 to 3.0
+
+3 way connection handshake:: When a Client establishes a connection, after
+receiving the <<msg-server-hello-complete>> message and sending its local
+entries, it finishes with a <<msg-client-hello-complete>> message to the
+server. This enables the Server to be aware of when the Client is fully
+synchronized.
+
+String length encoding:: String length is now encoded as unsigned <<leb128>>
+rather than as a 2-byte unsigned integer. This both allows string lengths
+longer than 64K and is more space efficient for the common case of short
+strings (<128 byte strings only require a single byte for length).
+
+Entry deletion:: Entries may now be deleted by any member of the Network using
+the <<msg-delete>> and <<msg-clear-all>> messages. Note that in a Network
+consisting of mixed 2.0 and 3.0 Clients, deletion may be ineffective because
+the deletion message will not be propagated to the 2.0 Clients.
+
+// TODO: needs more description in the text of how these messages are
+// propagated
+
+Remote procedure call:: The Server may create specially-typed entries that
+inform Clients of remotely callable functions on the Server. Clients can then
+execute these functions via the Network Tables protocol. See <<rpc-operation>>.
+
+Raw data type:: An arbitrary data type has been added. While string could be
+used to encode raw data, the reason for a different data type is so that
+dashboards can choose not to display the raw data (or display it in a different
+format).
+
+Client and server self-identification:: Clients self-identify with a
+user-defined string name when connecting to the Server (this is part of the new
+<<msg-client-hello-complete>> message). This provides a more reliable method
+than simply the remote IP address for determining on the Server side whether or
+not a particular Client is connected.  While Clients are less likely to care
+what Server they are connected to, for completeness a similar Server
+self-identification string has been added to the Server Hello Complete message.
+Note that Server connection information is not provided from the Server to
+Clients (at least in a way built into the protocol), so it is not possible for
+a Client to determine what other Clients are connected to the Server.
+
+Server reboot detection:: The Server keeps an internal list of all Client
+identity strings that have ever connected to it (this list is always empty at
+Server start). During the initial connection process, the Server sends the
+Client a flag (as part of the new <<msg-server-hello>> message) that indicates
+whether or not the Client was already on this list. Clients use this flag to
+determine whether the Server has rebooted since the previous connection.
+
+Entry flags:: Each Entry now has an 8-bit flags value associated with it (see
+<<entry-flags>>). The initial value of the flags are provided as part of the
+<<msg-assign>> message. The value of the flags may be updated by any member of
+the Network via use of the <<msg-flags-update>> message.
+
+Entry persistence:: The Server is required to provide a feature to
+automatically save entries (including their last known values) across Server
+restarts. By default, no values are automatically saved in this manner, but
+any member of the Network may set the “Persistent” Entry Flag on an Entry to
+indicate to the server that the Entry must be persisted by the Server. The
+Server must periodically save such flagged Entries to a file; on Server start,
+the Server reads the file to create the initial set of Server Entries.
+
+More robust Entry Update message encoding:: The entry type has been added to
+the <<msg-update>> message. This is used only to specify the length of value
+encoded in the Entry Update message, and has no effect on the Client or Server
+handling of Entry Updates. Clients and Servers must ignore Entry Update
+messages with mismatching type to their currently stored value. This increases
+robustness of Entry Updates in the presence of Entry Assignments with varying
+type (which should be uncommon, but this fixes a weakness in the 2.0 protocol).
+
+////
+TODO
+
+Synchronization on reconnect:: The approach to how Clients should handle
+conflicting values when reconnecting to a Server has been clarified.
+
+////
+
+[[references]]
+== References
+
+[[networktables2]]
+* <<networktables2.adoc#,Network Tables Protocol Specification, Protocol
+Revision 2.0 (0x0200)>>, dated 1/8/2013.
+
+[[leb128,LEB128]]
+* LEB128 definition in DWARF Specification 3.0
+(http://dwarfstd.org/doc/Dwarf3.pdf, section 7.6 and Appendix C, also explained
+in http://en.wikipedia.org/wiki/LEB128)
+
+[[rfc1982,RFC1982]]
+* RFC 1982, Serial Number Arithmetic, http://tools.ietf.org/html/rfc1982
+
+[[rfc2119,RFC2119]]
+* RFC 2119, Key words for use in RFCs to Indicate Requirement Levels,
+http://tools.ietf.org/html/rfc2119
+
+[[definitions]]
+== Definitions
+
+[[def-client]]
+Client:: An implementation of this protocol running in client configuration.
+Any number of Clients may exist for a given Network.
+
+[[def-entry]]
+Entry:: A data value identified by a string name.
+
+[[def-entry-id]]
+Entry ID:: An unsigned 2-byte ID by which the Server and Clients refer to an
+Entry across the network instead of using the full string key for the Entry.
+Entry IDs range from 0x0000 to 0xFFFE (0xFFFF is reserved for an Entry
+Assignment issued by a Client).
+
+[[def-server]]
+Server:: An implementation of this protocol running in server configuration.
+One and only one Server must exist for a given Network.
+
+[[def-network]]
+Network:: One or more Client nodes connected to a Server.
+
+[[def-user-code]]
+User Code:: User-supplied code which may interact with a Client or Server. User
+Code should be executed on the same computer as the Client or Server instance
+it interacts with.
+
+[[def-sequence-number]]
+Sequence Number:: An unsigned number which allows the Server to resolve update
+conflicts between Clients and/or the Server. Sequence numbers may overflow.
+Sequential arithmetic comparisons, which must be used with Sequence Numbers,
+are defined by <<rfc1982>>.
+
+[[def-protocol-revision]]
+Protocol Revision:: A 16-bit unsigned integer which indicates the version of
+the network tables protocol that a client wishes to use. The protocol revision
+assigned to this version of the network tables specification is listed at the
+top of this document. This number is listed in dot-decimal notation as well as
+its equivalent hexadecimal value.
+
+== Transport Layer
+
+Conventional implementations of this protocol should use TCP for reliable
+communication; the Server should listen on TCP port 1735 for incoming
+connections.
+
+== Example Exchanges
+
+[[exchange-connect]]
+=== Client Connects to the Server
+
+Directly after client establishes a connection with the Server, the following
+procedure must be followed:
+
+. The Client sends a <<msg-client-hello>> message to the Server
+
+. The Server sends a <<msg-server-hello>> message.
+
+. The Server sends one <<msg-assign>> for every field it currently recognizes.
+
+. The Server sends a <<msg-server-hello-complete>> message.
+
+. For all Entries the Client recognizes that the Server did not identify with a
+Entry Assignment, the client follows the <<exchange-client-creates-entry>>
+protocol.
+
+. The Client sends a <<msg-client-hello-complete>> message.
+
+In the event that the Server does not support the protocol revision that the
+Client has requested in a Client Hello message, the Server must instead issue a
+<<msg-protocol-unsupported>> message to the joining client and close the
+connection.
+
+[[exchange-client-creates-entry]]
+=== Client Creates an Entry
+
+When User Code on a Client assigns a value to an Entry that the Server has not
+yet issued a Entry Assignment for, the following procedure must be followed:
+
+. The Client sends an <<msg-assign>> with an Entry ID of 0xFFFF.
+
+. The Server issues an <<msg-assign>> to all Clients (including the sender) for
+the new field containing a real Entry ID and Sequence Number for the new field.
+
+In the event that User Code on the Client updates the value of the
+to-be-announced field again before the expected Entry Assignment is received,
+then the Client must save the new value and take no other action (the most
+recent value of the field should be issued when the Entry Assignment arrives,
+if it differs from the value contained in the received Entry Assignment).
+
+In the event that the Client receives a Entry Assignment from the Server for
+the Entry that it intended to issue an Entry Assignment for, before it issued
+its own Entry Assignment, the procedure may end early.
+
+In the event that the Server receives a duplicate Entry Assignment from a
+Client (likely due to the client having not yet received the Server's Entry
+Assignment), the Server should ignore the duplicate Entry Assignment.
+
+[[exchange-client-updates-entry]]
+=== Client Updates an Entry
+
+When User Code on a Client updates the value of an Entry, the Client must send
+an <<msg-update>> message to the Server. The Sequence Number included in the
+Entry Update message must be the most recently received Sequence Number for the
+Entry to be updated incremented by one.
+
+.Example:
+
+. Client receives Entry Assignment message for Entry "a" with integer value 1,
+Entry ID of 0, and Sequence Number 1.
+
+. User Code on Client updates value of Entry "a" to 16 (arbitrary).
+
+. Client sends Entry Update message to Server for Entry 0 with a Sequence
+Number of 2 and a value of 16.
+
+When the Server receives an Entry Update message, it first checks the Sequence
+Number in the message against the Server's value for the Sequence Number
+associated with the Entry to be updated. If the received Sequence Number is
+strictly greater than (aside: see definition of "greater than" under the
+definition of Sequence Number) the Server's Sequence Number for the Entry to be
+updated, the Server must apply the new value for the indicated Entry and repeat
+the Entry Update message to all other connected Clients.
+
+If the received Sequence Number is less than or equal (see definition of "less
+than or equal" in RFC 1982) to the Server's Sequence Number for the Entry to be
+updated, this implies that the Client which issued the Entry Update message has
+not yet received one or more Entry Update message(s) that the Server recently
+sent to it; therefore, the Server must ignore the received Entry Update
+message. In the event that comparison between two Sequence Numbers is undefined
+(see RFC 1982), then the Server must always win (it ignores the Entry Update
+message under consideration).
+
+[[update-rate]]
+NOTE: If User Code modifies the value of an Entry too quickly, 1) users may not
+see every value appear on remote machines, and 2) the consistency protection
+offered by the Entry's Sequence Number may be lost (by overflowing before
+remote devices hear recent values). It is recommended that implementations
+detect when user code updates an Entry more frequently than once every 5
+milliseconds and print a warning message to the user (and/or offer some other
+means of informing User Code of this condition).
+
+[[exchange-client-updates-flags]]
+=== Client Updates an Entry's Flags
+
+When User Code on a Client updates an Entry's flags, the Client must apply the
+new flags to the Entry immediately, and send an <<msg-flags-update>> message to
+the Server.
+
+When the Server receives an Entry Flags Update message, it must apply the new
+flags to the indicated Entry and repeat the Entry Flags Update message to all
+other connected Clients.
+
+[[exchange-client-deletes-entry]]
+=== Client Deletes an Entry
+
+When User Code on a Client deletes an Entry, the Client must immediately delete
+the Entry, and send an <<msg-delete>> message to the Server.
+
+When the Server receives an Entry Delete message, it must delete the indicated
+Entry and repeat the Entry Delete message to all other connected Clients.
+
+[[exchange-server-creates-entry]]
+=== Server Creates an Entry
+
+When User Code on the Server assigns a value to a Entry which does not exist,
+the Server must issue an <<msg-assign>> message to all connected clients.
+
+[[exchange-server-updates-entry]]
+=== Server Updates an Entry
+
+When User Code on the Server updates the value of an Entry, the Server must
+apply the new value to the Entry immediately, increment the associated Entry's
+Sequence Number, and issue a <<msg-update>> message containing the new value
+and Sequence Number of the associated Entry to all connected Clients.
+
+NOTE: See <<update-rate,Note>> under <<exchange-client-updates-entry>>.
+
+[[exchange-server-updates-flags]]
+=== Server Updates an Entry's Flags
+
+When User Code on the Server updates an Entry's flags, the Server must apply
+the new flags to the Entry immediately, and issue a <<msg-flags-update>>
+message containing the new flags value to all connected Clients.
+
+[[exchange-server-deletes-entry]]
+=== Server Deletes an Entry
+
+When User Code on the Server deletes an Entry, the Server must immediately
+delete the Entry, and issue a <<msg-delete>> message to all connected Clients.
+
+[[exchange-keep-alive]]
+=== Keep Alive
+
+To maintain a connection and prove a socket is still open, a Client or Server
+may issue <<msg-keep-alive>> messages. Clients and the Server should ignore
+incoming Keep Alive messages.
+
+The intent is that by writing a Keep Alive to a socket, a Client forces its
+network layer (TCP) to reevaluate the state of the network connection as it
+attempts to deliver the Keep Alive message. In the event that a connection is
+no longer usable, a Client's network layer should inform the Client that it is
+no longer usable within a few attempts to send a Keep Alive message.
+
+To provide timely connection status information, Clients should send a Keep
+Alive message to the Server after every 1 second period of connection
+inactivity (i.e. no information is being sent to the Server). Clients should
+not send Keep Alive messages more frequently than once every 100 milliseconds.
+
+Since the Server does not require as timely information about the status of a
+connection, it is not required to send Keep Alive messages during a period of
+inactivity.
+
+[[bandwidth]]
+== Bandwidth and Latency Considerations
+
+To reduce unnecessary bandwidth usage, implementations of this protocol should:
+
+* Send an Entry Update if and only if the value of an Entry is changed to a
+value that is different from its prior value.
+
+* Buffer messages and transmit them in groups, when possible, to reduce
+transport protocol overhead.
+
+* Only send the most recent value of an Entry. When User Code updates the value
+of an Entry more than once before the new value is transmitted, only the latest
+value of the Entry should be sent.
+
+It is important to note that these behaviors will increase the latency between
+when a Client or Server updates the value of an Entry and when all Clients
+reflect the new value. The exact behavior of this buffering is left to
+implementations to determine, although the chosen scheme should reflect the
+needs of User Code. Implementations may include a method by which User Code can
+specify the maximum tolerable send latency.
+
+[[entry-types]]
+== Entry Types
+
+Entry Type must assume one the following values:
+
+[cols="1,3"]
+|===
+|Numeric Value |Type
+
+|0x00
+|Boolean
+
+|0x01
+|Double
+
+|0x02
+|String
+
+|0x03
+|Raw Data
+
+|0x10
+|Boolean Array
+
+|0x11
+|Double Array
+
+|0x12
+|String Array
+
+|0x20
+|Remote Procedure Call Definition
+|===
+
+[[entry-values]]
+== Entry Values
+
+Entry Value must assume the following structure as indicated by Entry Type:
+
+[cols="1,3"]
+|===
+|Entry Type |Entry Value Format
+
+|[[entry-value-boolean]]Boolean
+|1 byte, unsigned; True = 0x01, False = 0x00
+
+|[[entry-value-double]]Double
+|8 bytes, IEEE 754 floating-point "double format" bit layout; (big endian)
+
+|[[entry-value-string]]String
+|N bytes, unsigned <<leb128>> encoded length of the number of raw bytes to
+follow, followed by the string encoded in UTF-8
+
+|[[entry-value-raw]]Raw Data
+|N bytes, unsigned LEB128 encoded length of the number of raw bytes to follow,
+followed by the raw bytes.
+
+While the raw data definition is unspecified, it's recommended that users use
+the first byte of the raw data to "tag" the type of data actually being stored.
+
+|[[entry-value-boolean-array]]Boolean Array
+|1 byte, unsigned, number of elements within the array to follow
+
+N bytes - The raw bytes representing each Boolean element contained within the
+array, beginning with the item at index 0 within the array.
+
+|[[entry-value-double-array]]Double Array
+|1 byte, unsigned, number of elements within the array to follow
+
+N bytes - The raw bytes representing each Double element contained within the
+array, beginning with the item at index 0 within the array.
+
+|[[entry-value-string-array]]String Array
+|1 byte, unsigned, number of elements within the array to follow
+
+N bytes - The raw bytes representing each String element contained within the
+array, beginning with the item at index 0 within the array.
+
+|[[entry-value-rpc]]Remote Procedure Call Definition
+|N bytes, unsigned LEB128 encoded length of the number of raw bytes to follow.
+
+N bytes - data as defined in Remote Procedure Call Definition Data
+|===
+
+[[entry-flags]]
+== Entry Flags
+
+Entry Flags are as follows:
+
+[cols="1,3"]
+|===
+|Bit Mask |Bit Value Meaning
+
+|[[entry-flag-persistent]]0x01 (least significant bit) - Persistent
+
+|0x00: Entry is not persistent. The entry and its value will not be retained
+across a server restart.
+
+0x01: Entry is persistent. Updates to the value are automatically saved and
+the entry will be automatically created and the last known value restored when
+the server starts.
+
+|0xFE
+|Reserved
+|===
+
+== Message Structures
+
+All messages are of the following format:
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|Message Type
+|1 byte, unsigned
+
+|Message Data
+|N bytes (length determined by Message Type)
+|===
+
+[[msg-keep-alive]]
+=== Keep Alive
+
+Indicates that the remote party is checking the status of a network connection.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x00 - Keep Alive
+|1 byte, unsigned; Message Type
+|===
+
+[[msg-client-hello]]
+=== Client Hello
+
+A Client issues a Client Hello message when first establishing a connection.
+The Client Protocol Revision field specifies the Network Table protocol
+revision that the Client would like to use.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x01 - Client Hello
+|1 byte, unsigned; Message Type
+
+|Client Protocol Revision
+|2 bytes, Unsigned 16-bit integer (big-endian). See
+<<def-protocol-revision,Protocol Revision>>.
+
+|Client identity (name)
+|<<entry-value-string,String>>
+|===
+
+[[msg-protocol-unsupported]]
+=== Protocol Version Unsupported
+
+A Server issues a Protocol Version Unsupported message to a Client to inform it
+that the requested protocol revision is not supported. It also includes the
+most recent protocol revision which it supports, such that a Client may
+reconnect under a prior protocol revision if able.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x02 - Protocol Version Unsupported
+|1 byte, unsigned; Message Type
+
+|Server Supported Protocol Revision
+|2 bytes, Unsigned 16-bit integer (big-endian). See
+<<def-protocol-revision,Protocol Revision>>.
+|===
+
+[[msg-server-hello-complete]]
+=== Server Hello Complete
+
+A Server issues a Server Hello Complete message when it has finished informing
+a newly-connected client of all of the fields it currently recognizes.
+Following the receipt of this message, a Client should inform the Server of
+any/all additional fields that it recognizes that the Server did not announce.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x03 - Server Hello Complete
+|1 byte, unsigned; Message Type
+|===
+
+[[msg-server-hello]]
+=== Server Hello
+
+A Server issues a Server Hello message in response to a Client Hello message,
+immediately prior to informing a newly-connected client of all of the fields it
+currently recognizes.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x04 - Server Hello
+|1 byte, unsigned; Message Type
+
+|Flags
+a|1 byte, unsigned.
+
+Least Significant Bit (bit 0): reconnect flag
+
+* 0 if this is the first time (since server start) the server has seen the
+client
+
+* 1 if the server has previously seen (since server start) the client (as
+identified in the <<msg-client-hello,Client Hello>> message)
+
+Bits 1-7: Reserved, set to 0.
+
+|Server identity (name)
+|<<entry-value-string,String>>
+|===
+
+[[msg-client-hello-complete]]
+=== Client Hello Complete
+
+A Client issues a Client Hello Complete message when it has finished informing
+the Server of any/all of the additional fields it recognizes that the Server
+did not announce.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x05 - Client Hello Complete
+|1 byte, unsigned; Message Type
+|===
+
+[[msg-assign]]
+=== Entry Assignment
+
+A Entry Assignment message informs the remote party of a new Entry. An Entry
+Assignment's value field must be the most recent value of the field being
+assigned at the time that the Entry Assignment is sent.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x10 - Entry Assignment
+|1 byte, unsigned; Message Type
+
+|Entry Name
+|<<entry-value-string,String>>
+
+|Entry Type
+|1 byte, unsigned; see <<entry-types,Entry Types>>
+
+|Entry ID
+|2 bytes, unsigned
+
+|Entry Sequence Number
+|2 bytes, unsigned
+
+|Entry Flags
+|1 byte, unsigned; see <<entry-flags,Entry Flags>>
+
+|Entry Value
+|N bytes, length depends on Entry Type
+|===
+
+If the Entry ID is 0xFFFF, then this assignment represents a request from a
+Client to the Server. In this event, the Entry ID field and the Entry Sequence
+Number field must not be stored or relied upon as they otherwise would be.
+
+[[msg-update]]
+=== Entry Update
+
+An Entry Update message informs a remote party of a new value for an Entry.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x11 - Entry Update
+|1 byte, unsigned; Message Type
+
+|Entry ID
+|2 bytes, unsigned
+
+|Entry Sequence Number
+|2 bytes, unsigned
+
+|Entry Type
+|1 byte, unsigned; see <<entry-types,Entry Types>>.
+
+Note this type is only used to determine the length of the entry value, and
+does NOT change the stored entry type if it is different (due to an intervening
+Entry Assignment); Clients and Servers must ignore Entry Update messages with
+mismatching entry type.
+
+|Entry Value
+|N bytes, length dependent on value type
+|===
+
+[[msg-flags-update]]
+=== Entry Flags Update
+
+An Entry Flags Update message informs a remote party of new flags for an Entry.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x12 - Entry Flags Update
+|1 byte, unsigned; Message Type
+
+|Entry ID
+|2 bytes, unsigned
+
+|Entry Flags
+|1 byte, unsigned; see <<entry-flags,Entry Flags>>
+|===
+
+Entries may be globally deleted using the following messages. These messages
+must be rebroadcast by the server in the same fashion as the Entry Update
+message. Clients and servers must remove the requested entry/entries from
+their local tables. Update messages received after the Entry Delete message
+for the deleted Entry ID must be ignored by Clients and Servers until a new
+Assignment message for that Entry ID is issued.
+
+[[msg-delete]]
+=== Entry Delete
+
+Deletes a single entry or procedure.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x13 - Entry Delete
+|1 byte, unsigned; message type
+
+|Entry ID
+|2 bytes, unsigned
+|===
+
+[[msg-clear-all]]
+=== Clear All Entries
+
+Deletes all entries. The magic value is required to be exactly this value
+(this is to avoid accidental misinterpretation of the message).
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x14 - Clear All Entries
+|1 byte, unsigned; message type
+
+|Magic Value (0xD06CB27A)
+|4 bytes; exact value required (big endian)
+|===
+
+[[msg-rpc-execute]]
+=== Remote Procedure Call (RPC) Execute
+
+Executes a remote procedure. Intended for client to server use only.
+
+The client shall provide a value for every RPC parameter specified in the
+corresponding RPC entry definition.
+
+The server shall ignore any Execute RPC message whose decoding does not match
+the parameters defined in the corresponding RPC entry definition.
+
+Note that the parameter length is encoded the same way regardless of the RPC
+version and encapsulates the entirety of the parameters, so protocol layer
+decoders do not need to know the RPC details in order to process the message.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x20 - Execute RPC
+|1 byte, unsigned; message type
+
+|RPC Definition Entry ID
+|2 bytes, unsigned
+
+|Unique ID
+|2 bytes, unsigned; incremented value for matching return values to call.
+
+|Parameter Value Length
+|N bytes, unsigned <<leb128>> encoded length of:
+
+RPC definition version 0: total number of raw bytes in this message
+
+RPC definition version 1: total number of bytes of parameter values in this
+message
+|Parameter Value(s)
+|RPC definition version 0: N raw bytes.
+
+RPC definition version 1: Array of values; N bytes for each parameter (length
+dependent on the parameter type defined in the
+<<rpc-definition,RPC entry definition>>).
+|===
+
+[[msg-rpc-response]]
+=== RPC Response
+
+Return responses from a remote procedure call. Even calls with zero outputs
+will respond.
+
+Note that the result length is encoded the same way regardless of the RPC
+version and encapsulates the entirety of the result, so protocol layer decoders
+do not need to know the RPC details in order to process the message.
+
+[cols="1,3"]
+|===
+|Field Name |Field Type
+
+|0x21 - RPC Response
+|1 byte, unsigned; message type
+
+|RPC Definition Entry ID
+|2 bytes, unsigned
+
+|Unique ID
+|2 bytes, unsigned; matching ID from <<msg-rpc-execute,RPC Execute>> message
+
+|Result Value Length
+|N bytes, unsigned <<leb128>> encoded length of:
+
+RPC definition version 0: total number of raw bytes in this message
+
+RPC definition version 1: total number of bytes of result values in this
+message
+|Result Value(s)
+|RPC definition version 0: N raw bytes.
+
+RPC definition version 1: Array of values; N bytes for each result (length
+dependent on the result type defined in the
+<<rpc-definition,RPC entry definition>>).
+|===
+
+[[rpc-operation]]
+== Remote Procedure Call (RPC) Operation
+
+Remote procedure call entries shall only be assigned by the server.
+
+Remote procedure call execute messages will result in asynchronous execution of
+the corresponding function on the server.
+
+Client implementations shall not transmit an Execute RPC message and return an
+error to user code that attempts to call an undefined RPC, call one with
+incorrectly typed parameters, or attempts to make a call when the Client is not
+connected to a Server.
+
+Remote procedure calls cannot be persisted.
+
+[[rpc-definition]]
+=== Remote Procedure Call Definition Data
+
+There are currently two versions of RPC definitions: version 0 and version 1.
+The first byte in the RPC definition entry determines the version.
+
+[[rpc-definition-v0]]
+==== Version 0
+
+RPC version 0 is the most straightforward: the data provided in the RPC
+definition entry consists of just a single 0 byte (indicating RPC
+definition version 0).  RPC version 0 execute and response messages do
+not contain discrete parameter and result values respectively; instead the
+entire parameter value or result value is treated as a raw byte sequence; the
+interpretation of the raw bytes is application specific--users are encouraged
+to consider using encodings such as CBOR or MessagePack for more complex
+self-describing data structures.
+
+[cols="1,3"]
+|===
+|RPC Definition Version
+|1 byte, unsigned; set to 0, indicating version 0
+|===
+
+[[rpc-definition-v1]]
+==== Version 1
+
+The data provided in the RPC version 1 definition entry is more
+complex and consists of:
+
+[cols="1,3"]
+|===
+|RPC Definition Version
+|1 byte, unsigned; set to 1, indicating version 1
+
+|Procedure (Entry) Name
+|<<entry-value-string,String>>
+
+|Number of Parameters
+|1 byte, unsigned (may be 0)
+
+2+s|Parameter Specification (one set per input parameter)
+
+|Parameter Type
+|1 byte, unsigned; <<entry-types,Entry Type>> for parameter value
+
+|Parameter Name
+|<<entry-value-string,String>>
+
+|Parameter Default Value
+|N bytes; length based on parameter type (encoded consistent with corresponding
+<<entry-values,Entry Value>> definition)
+
+|Number of Output Results
+|1 byte, unsigned (may be 0)
+
+2+s|Result Specification (one set per output)
+
+|Result Type
+|1 byte, unsigned; <<entry-types,Entry Type>> for value
+
+|Result Name
+|<<entry-value-string,String>>
+|===
diff --git a/ntcore/manualTests/java/Client.java b/ntcore/manualTests/java/Client.java
new file mode 100644
index 0000000..1d5f181
--- /dev/null
+++ b/ntcore/manualTests/java/Client.java
@@ -0,0 +1,46 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+import edu.wpi.first.wpilibj.networktables.*;
+import edu.wpi.first.wpilibj.tables.*;
+
+public class Client {
+  private static class MyLogger implements NetworkTablesJNI.LoggerFunction {
+    public void apply(int level, String file, int line, String msg) {
+      System.err.println(msg);
+    }
+  }
+
+  public static void main(String[] args) {
+    NetworkTablesJNI.setLogger(new MyLogger(), 0);
+    NetworkTable.setIPAddress("127.0.0.1");
+    NetworkTable.setPort(10000);
+    NetworkTable.setClientMode();
+    NetworkTable nt = NetworkTable.getTable("");
+    try { Thread.sleep(2000); } catch (InterruptedException e) {}
+    try {
+      System.out.println("Got foo: " + nt.getNumber("foo"));
+    } catch(TableKeyNotDefinedException ex) {
+    }
+    nt.putBoolean("bar", false);
+    nt.setFlags("bar", NetworkTable.PERSISTENT);
+    nt.putBoolean("bar2", true);
+    nt.putBoolean("bar2", false);
+    nt.putBoolean("bar2", true);
+    nt.putString("str", "hello world");
+    double[] nums = new double[3];
+    nums[0] = 0.5;
+    nums[1] = 1.2;
+    nums[2] = 3.0;
+    nt.putNumberArray("numarray", nums);
+    String[] strs = new String[2];
+    strs[0] = "Hello";
+    strs[1] = "World";
+    nt.putStringArray("strarray", strs);
+    try { Thread.sleep(10000); } catch (InterruptedException e) {}
+  }
+}
diff --git a/ntcore/manualTests/java/Server.java b/ntcore/manualTests/java/Server.java
new file mode 100644
index 0000000..7f4dbc6
--- /dev/null
+++ b/ntcore/manualTests/java/Server.java
@@ -0,0 +1,33 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+import edu.wpi.first.wpilibj.networktables.*;
+import edu.wpi.first.wpilibj.tables.*;
+
+public class Server {
+  private static class MyLogger implements NetworkTablesJNI.LoggerFunction {
+    public void apply(int level, String file, int line, String msg) {
+      System.err.println(msg);
+    }
+  }
+
+  public static void main(String[] args) {
+    NetworkTablesJNI.setLogger(new MyLogger(), 0);
+    NetworkTable.setIPAddress("127.0.0.1");
+    NetworkTable.setPort(10000);
+    NetworkTable.setServerMode();
+    NetworkTable nt = NetworkTable.getTable("");
+    try { Thread.sleep(1000); } catch (InterruptedException e) {}
+    nt.putNumber("foo", 0.5);
+    nt.setFlags("foo", NetworkTable.PERSISTENT);
+    nt.putNumber("foo2", 0.5);
+    nt.putNumber("foo2", 0.7);
+    nt.putNumber("foo2", 0.6);
+    nt.putNumber("foo2", 0.5);
+    try { Thread.sleep(10000); } catch (InterruptedException e) {}
+  }
+}
diff --git a/ntcore/manualTests/native/client.cpp b/ntcore/manualTests/native/client.cpp
new file mode 100644
index 0000000..3bcb7c0
--- /dev/null
+++ b/ntcore/manualTests/native/client.cpp
@@ -0,0 +1,40 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <chrono>
+#include <climits>
+#include <cstdio>
+#include <thread>
+
+#include "ntcore.h"
+
+int main() {
+  auto inst = nt::GetDefaultInstance();
+  nt::AddLogger(inst,
+                [](const nt::LogMessage& msg) {
+                  std::fputs(msg.message.c_str(), stderr);
+                  std::fputc('\n', stderr);
+                },
+                0, UINT_MAX);
+  nt::StartClient(inst, "127.0.0.1", 10000);
+  std::this_thread::sleep_for(std::chrono::seconds(2));
+
+  auto foo = nt::GetEntry(inst, "/foo");
+  auto foo_val = nt::GetEntryValue(foo);
+  if (foo_val && foo_val->IsDouble())
+    std::printf("Got foo: %g\n", foo_val->GetDouble());
+
+  auto bar = nt::GetEntry(inst, "/bar");
+  nt::SetEntryValue(bar, nt::Value::MakeBoolean(false));
+  nt::SetEntryFlags(bar, NT_PERSISTENT);
+
+  auto bar2 = nt::GetEntry(inst, "/bar2");
+  nt::SetEntryValue(bar2, nt::Value::MakeBoolean(true));
+  nt::SetEntryValue(bar2, nt::Value::MakeBoolean(false));
+  nt::SetEntryValue(bar2, nt::Value::MakeBoolean(true));
+  std::this_thread::sleep_for(std::chrono::seconds(10));
+}
diff --git a/ntcore/manualTests/native/rpc_local.cpp b/ntcore/manualTests/native/rpc_local.cpp
new file mode 100644
index 0000000..b8352d6
--- /dev/null
+++ b/ntcore/manualTests/native/rpc_local.cpp
@@ -0,0 +1,66 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <chrono>
+#include <climits>
+#include <cstdio>
+#include <thread>
+
+#include <support/json.h>
+
+#include "ntcore.h"
+
+void callback1(const nt::RpcAnswer& answer) {
+  wpi::json params;
+  try {
+    params = wpi::json::from_cbor(answer.params);
+  } catch (wpi::json::parse_error err) {
+    std::fputs("could not decode params?\n", stderr);
+    return;
+  }
+  if (!params.is_number()) {
+    std::fputs("did not get number\n", stderr);
+    return;
+  }
+  double val = params.get<double>();
+  std::fprintf(stderr, "called with %g\n", val);
+
+  answer.PostResponse(wpi::json::to_cbor(val + 1.2));
+}
+
+int main() {
+  auto inst = nt::GetDefaultInstance();
+  nt::AddLogger(inst,
+                [](const nt::LogMessage& msg) {
+                  std::fputs(msg.message.c_str(), stderr);
+                  std::fputc('\n', stderr);
+                },
+                0, UINT_MAX);
+
+  nt::StartServer(inst, "rpc_local.ini", "", 10000);
+  auto entry = nt::GetEntry(inst, "func1");
+  nt::CreateRpc(entry, nt::StringRef("", 1), callback1);
+  std::fputs("calling rpc\n", stderr);
+  unsigned int call1_uid = nt::CallRpc(entry, wpi::json::to_cbor(2.0));
+  std::string call1_result_str;
+  std::fputs("waiting for rpc result\n", stderr);
+  nt::GetRpcResult(entry, call1_uid, &call1_result_str);
+  wpi::json call1_result;
+  try {
+    call1_result = wpi::json::from_cbor(call1_result_str);
+  } catch (wpi::json::parse_error err) {
+    std::fputs("could not decode result?\n", stderr);
+    return 1;
+  }
+  if (!call1_result.is_number()) {
+    std::fputs("result is not number?\n", stderr);
+    return 1;
+  }
+  std::fprintf(stderr, "got %g\n", call1_result.get<double>());
+
+  return 0;
+}
diff --git a/ntcore/manualTests/native/rpc_speed.cpp b/ntcore/manualTests/native/rpc_speed.cpp
new file mode 100644
index 0000000..70558ef
--- /dev/null
+++ b/ntcore/manualTests/native/rpc_speed.cpp
@@ -0,0 +1,75 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <chrono>
+#include <climits>
+#include <cstdio>
+#include <iostream>
+#include <thread>
+
+#include <support/json.h>
+
+#include "ntcore.h"
+
+void callback1(const nt::RpcAnswer& answer) {
+  wpi::json params;
+  try {
+    params = wpi::json::from_cbor(answer.params);
+  } catch (wpi::json::parse_error err) {
+    std::fputs("could not decode params?\n", stderr);
+    return;
+  }
+  if (!params.is_number()) {
+    std::fputs("did not get number\n", stderr);
+    return;
+  }
+  double val = params.get<double>();
+  answer.PostResponse(wpi::json::to_cbor(val + 1.2));
+}
+
+int main() {
+  auto inst = nt::GetDefaultInstance();
+  nt::StartServer(inst, "rpc_speed.ini", "", 10000);
+  auto entry = nt::GetEntry(inst, "func1");
+  nt::CreateRpc(entry, nt::StringRef("", 1), callback1);
+  std::string call1_result_str;
+
+  auto start2 = std::chrono::high_resolution_clock::now();
+  auto start = nt::Now();
+  for (int i = 0; i < 10000; ++i) {
+    unsigned int call1_uid = nt::CallRpc(entry, wpi::json::to_cbor(i));
+    nt::GetRpcResult(entry, call1_uid, &call1_result_str);
+    wpi::json call1_result;
+    try {
+      call1_result = wpi::json::from_cbor(call1_result_str);
+    } catch (wpi::json::parse_error err) {
+      std::fputs("could not decode result?\n", stderr);
+      return 1;
+    }
+    if (!call1_result.is_number()) {
+      std::fputs("result is not number?\n", stderr);
+      return 1;
+    }
+  }
+  auto end2 = std::chrono::high_resolution_clock::now();
+  auto end = nt::Now();
+  std::cerr << "nt::Now start=" << start << " end=" << end << '\n';
+  std::cerr << "std::chrono start="
+            << std::chrono::duration_cast<std::chrono::nanoseconds>(
+                   start2.time_since_epoch())
+                   .count()
+            << " end="
+            << std::chrono::duration_cast<std::chrono::nanoseconds>(
+                   end2.time_since_epoch())
+                   .count()
+            << '\n';
+  std::fprintf(stderr, "time/call = %g us\n", (end - start) / 10.0 / 10000.0);
+  std::chrono::duration<double, std::micro> diff = end2 - start2;
+  std::cerr << "time/call = " << (diff.count() / 10000.0) << " us\n";
+
+  return 0;
+}
diff --git a/ntcore/manualTests/native/server.cpp b/ntcore/manualTests/native/server.cpp
new file mode 100644
index 0000000..9513bf8
--- /dev/null
+++ b/ntcore/manualTests/native/server.cpp
@@ -0,0 +1,37 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <chrono>
+#include <climits>
+#include <cstdio>
+#include <thread>
+
+#include "ntcore.h"
+
+int main() {
+  auto inst = nt::GetDefaultInstance();
+  nt::AddLogger(inst,
+                [](const nt::LogMessage& msg) {
+                  std::fputs(msg.message.c_str(), stderr);
+                  std::fputc('\n', stderr);
+                },
+                0, UINT_MAX);
+  nt::StartServer(inst, "persistent.ini", "", 10000);
+  std::this_thread::sleep_for(std::chrono::seconds(1));
+
+  auto foo = nt::GetEntry(inst, "/foo");
+  nt::SetEntryValue(foo, nt::Value::MakeDouble(0.5));
+  nt::SetEntryFlags(foo, NT_PERSISTENT);
+
+  auto foo2 = nt::GetEntry(inst, "/foo2");
+  nt::SetEntryValue(foo2, nt::Value::MakeDouble(0.5));
+  nt::SetEntryValue(foo2, nt::Value::MakeDouble(0.7));
+  nt::SetEntryValue(foo2, nt::Value::MakeDouble(0.6));
+  nt::SetEntryValue(foo2, nt::Value::MakeDouble(0.5));
+
+  std::this_thread::sleep_for(std::chrono::seconds(10));
+}
diff --git a/ntcore/ntcore-config.cmake b/ntcore/ntcore-config.cmake
new file mode 100644
index 0000000..6be1dda
--- /dev/null
+++ b/ntcore/ntcore-config.cmake
@@ -0,0 +1,5 @@
+include(CMakeFindDependencyMacro)

+find_dependency(wpiutil)

+

+get_filename_component(SELF_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)

+include(${SELF_DIR}/ntcore.cmake)

diff --git a/ntcore/src/dev/java/edu/wpi/first/ntcore/DevMain.java b/ntcore/src/dev/java/edu/wpi/first/ntcore/DevMain.java
new file mode 100644
index 0000000..547af0a
--- /dev/null
+++ b/ntcore/src/dev/java/edu/wpi/first/ntcore/DevMain.java
@@ -0,0 +1,25 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.ntcore;
+
+import edu.wpi.first.networktables.NetworkTablesJNI;
+import edu.wpi.first.wpiutil.RuntimeDetector;
+
+public final class DevMain {
+  /**
+   * Main method.
+   */
+  public static void main(String[] args) {
+    System.out.println("Hello World!");
+    System.out.println(RuntimeDetector.getPlatformPath());
+    NetworkTablesJNI.flush(NetworkTablesJNI.getDefaultInstance());
+  }
+
+  private DevMain() {
+  }
+}
diff --git a/ntcore/src/dev/native/cpp/main.cpp b/ntcore/src/dev/native/cpp/main.cpp
new file mode 100644
index 0000000..1153347
--- /dev/null
+++ b/ntcore/src/dev/native/cpp/main.cpp
@@ -0,0 +1,18 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <iostream>
+
+#include "ntcore.h"
+
+int main() {
+  auto myValue = nt::GetEntry(nt::GetDefaultInstance(), "MyValue");
+
+  nt::SetEntryValue(myValue, nt::Value::MakeString("Hello World"));
+
+  std::cout << nt::GetEntryValue(myValue)->GetString() << std::endl;
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionInfo.java b/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionInfo.java
new file mode 100644
index 0000000..b060f2f
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionInfo.java
@@ -0,0 +1,64 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+/**
+ * NetworkTables Connection information.
+ */
+public final class ConnectionInfo {
+  /**
+   * The remote identifier (as set on the remote node by
+   * {@link NetworkTableInstance#setNetworkIdentity(String)}).
+   */
+  @SuppressWarnings("MemberName")
+  public final String remote_id;
+
+  /**
+   * The IP address of the remote node.
+   */
+  @SuppressWarnings("MemberName")
+  public final String remote_ip;
+
+  /**
+   * The port number of the remote node.
+   */
+  @SuppressWarnings("MemberName")
+  public final int remote_port;
+
+  /**
+   * The last time any update was received from the remote node (same scale as
+   * returned by {@link NetworkTablesJNI#now()}).
+   */
+  @SuppressWarnings("MemberName")
+  public final long last_update;
+
+  /**
+   * The protocol version being used for this connection.  This is in protocol
+   * layer format, so 0x0200 = 2.0, 0x0300 = 3.0).
+   */
+  @SuppressWarnings("MemberName")
+  public final int protocol_version;
+
+  /** Constructor.
+   * This should generally only be used internally to NetworkTables.
+   *
+   * @param remoteId Remote identifier
+   * @param remoteIp Remote IP address
+   * @param remotePort Remote port number
+   * @param lastUpdate Last time an update was received
+   * @param protocolVersion The protocol version used for the connection
+   */
+  public ConnectionInfo(String remoteId, String remoteIp, int remotePort, long lastUpdate,
+                        int protocolVersion) {
+    remote_id = remoteId;
+    remote_ip = remoteIp;
+    remote_port = remotePort;
+    last_update = lastUpdate;
+    protocol_version = protocolVersion;
+  }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionNotification.java b/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionNotification.java
new file mode 100644
index 0000000..129bf1a
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionNotification.java
@@ -0,0 +1,53 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+/**
+ * NetworkTables Connection notification.
+ */
+public final class ConnectionNotification {
+  /**
+   *  Listener that was triggered.
+   */
+  @SuppressWarnings("MemberName")
+  public final int listener;
+
+  /**
+   * True if event is due to connection being established.
+   */
+  @SuppressWarnings("MemberName")
+  public final boolean connected;
+
+  /**
+   * Connection information.
+   */
+  @SuppressWarnings("MemberName")
+  public final ConnectionInfo conn;
+
+  /** Constructor.
+   * This should generally only be used internally to NetworkTables.
+   *
+   * @param inst Instance
+   * @param listener Listener that was triggered
+   * @param connected Connected if true
+   * @param conn Connection information
+   */
+  public ConnectionNotification(NetworkTableInstance inst, int listener, boolean connected,
+                                ConnectionInfo conn) {
+    this.m_inst = inst;
+    this.listener = listener;
+    this.connected = connected;
+    this.conn = conn;
+  }
+
+  private final NetworkTableInstance m_inst;
+
+  public NetworkTableInstance getInstance() {
+    return m_inst;
+  }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/EntryInfo.java b/ntcore/src/main/java/edu/wpi/first/networktables/EntryInfo.java
new file mode 100644
index 0000000..b516cc7
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/EntryInfo.java
@@ -0,0 +1,71 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+/**
+ * NetworkTables Entry information.
+ */
+public final class EntryInfo {
+  /** Entry handle. */
+  @SuppressWarnings("MemberName")
+  public final int entry;
+
+  /** Entry name. */
+  @SuppressWarnings("MemberName")
+  public final String name;
+
+  /** Entry type. */
+  @SuppressWarnings("MemberName")
+  public final NetworkTableType type;
+
+  /** Entry flags. */
+  @SuppressWarnings("MemberName")
+  public final int flags;
+
+  /** Timestamp of last change to entry (type or value). */
+  @SuppressWarnings("MemberName")
+  public final long last_change;
+
+  /** Constructor.
+   * This should generally only be used internally to NetworkTables.
+   *
+   * @param inst Instance
+   * @param entry Entry handle
+   * @param name Name
+   * @param type Type (integer version of {@link NetworkTableType})
+   * @param flags Flags
+   * @param lastChange Timestamp of last change
+   */
+  public EntryInfo(NetworkTableInstance inst, int entry, String name, int type, int flags,
+                   long lastChange) {
+    this.m_inst = inst;
+    this.entry = entry;
+    this.name = name;
+    this.type = NetworkTableType.getFromInt(type);
+    this.flags = flags;
+    this.last_change = lastChange;
+  }
+
+  /* Network table instance. */
+  private final NetworkTableInstance m_inst;
+
+  /* Cached entry object. */
+  private NetworkTableEntry m_entryObject;
+
+  /**
+   * Get the entry as an object.
+   *
+   * @return NetworkTableEntry for this entry.
+   */
+  NetworkTableEntry getEntry() {
+    if (m_entryObject == null) {
+      m_entryObject = new NetworkTableEntry(m_inst, entry);
+    }
+    return m_entryObject;
+  }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/EntryListenerFlags.java b/ntcore/src/main/java/edu/wpi/first/networktables/EntryListenerFlags.java
new file mode 100644
index 0000000..9cdc0f0
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/EntryListenerFlags.java
@@ -0,0 +1,72 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+/**
+ * Flag values for use with entry listeners.
+ *
+ * <p>The flags are a bitmask and must be OR'ed together to indicate the
+ * combination of events desired to be received.
+ *
+ * <p>The constants kNew, kDelete, kUpdate, and kFlags represent different events
+ * that can occur to entries.
+ *
+ * <p>By default, notifications are only generated for remote changes occurring
+ * after the listener is created.  The constants kImmediate and kLocal are
+ * modifiers that cause notifications to be generated at other times.
+ */
+public interface EntryListenerFlags {
+  /**
+   * Initial listener addition.
+   *
+   * <p>Set this flag to receive immediate notification of entries matching the
+   * flag criteria (generally only useful when combined with kNew).
+   */
+  int kImmediate = 0x01;
+
+  /**
+   * Changed locally.
+   *
+   * <p>Set this flag to receive notification of both local changes and changes
+   * coming from remote nodes.  By default, notifications are only generated
+   * for remote changes.  Must be combined with some combination of kNew,
+   * kDelete, kUpdate, and kFlags to receive notifications of those respective
+   * events.
+   */
+  int kLocal = 0x02;
+
+  /**
+   * Newly created entry.
+   *
+   * <p>Set this flag to receive a notification when an entry is created.
+   */
+  int kNew = 0x04;
+
+  /**
+   * Entry was deleted.
+   *
+   * <p>Set this flag to receive a notification when an entry is deleted.
+   */
+  int kDelete = 0x08;
+
+  /**
+   * Entry's value changed.
+   *
+   * <p>Set this flag to receive a notification when an entry's value (or type)
+   * changes.
+   */
+  int kUpdate = 0x10;
+
+  /**
+   * Entry's flags changed.
+   *
+   * <p>Set this flag to receive a notification when an entry's flags value
+   * changes.
+   */
+  int kFlags = 0x20;
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/EntryNotification.java b/ntcore/src/main/java/edu/wpi/first/networktables/EntryNotification.java
new file mode 100644
index 0000000..159b968
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/EntryNotification.java
@@ -0,0 +1,82 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+/**
+ * NetworkTables Entry notification.
+ */
+public final class EntryNotification {
+  /**
+   * Listener that was triggered.
+   */
+  @SuppressWarnings("MemberName")
+  public final int listener;
+
+  /**
+   * Entry handle.
+   */
+  @SuppressWarnings("MemberName")
+  public final int entry;
+
+  /**
+   * Entry name.
+   */
+  @SuppressWarnings("MemberName")
+  public final String name;
+
+  /**
+   * The new value.
+   */
+  @SuppressWarnings("MemberName")
+  public final NetworkTableValue value;
+
+  /**
+   * Update flags.  For example, {@link EntryListenerFlags#kNew} if the key did
+   * not previously exist.
+   */
+  @SuppressWarnings("MemberName")
+  public final int flags;
+
+  /** Constructor.
+   * This should generally only be used internally to NetworkTables.
+   *
+   * @param inst Instance
+   * @param listener Listener that was triggered
+   * @param entry Entry handle
+   * @param name Entry name
+   * @param value The new value
+   * @param flags Update flags
+   */
+  public EntryNotification(NetworkTableInstance inst, int listener, int entry, String name,
+                           NetworkTableValue value, int flags) {
+    this.m_inst = inst;
+    this.listener = listener;
+    this.entry = entry;
+    this.name = name;
+    this.value = value;
+    this.flags = flags;
+  }
+
+  /* Network table instance. */
+  private final NetworkTableInstance m_inst;
+
+  /* Cached entry object. */
+  NetworkTableEntry m_entryObject;
+
+  /**
+   * Get the entry as an object.
+   *
+   * @return NetworkTableEntry for this entry.
+   */
+  public NetworkTableEntry getEntry() {
+    if (m_entryObject == null) {
+      m_entryObject = new NetworkTableEntry(m_inst, entry);
+    }
+    return m_entryObject;
+  }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/LogMessage.java b/ntcore/src/main/java/edu/wpi/first/networktables/LogMessage.java
new file mode 100644
index 0000000..2cf22ec
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/LogMessage.java
@@ -0,0 +1,82 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+/**
+ * NetworkTables log message.
+ */
+public final class LogMessage {
+  /**
+   * Logging levels.
+   */
+  public static final int kCritical = 50;
+  public static final int kError = 40;
+  public static final int kWarning = 30;
+  public static final int kInfo = 20;
+  public static final int kDebug = 10;
+  public static final int kDebug1 = 9;
+  public static final int kDebug2 = 8;
+  public static final int kDebug3 = 7;
+  public static final int kDebug4 = 6;
+
+  /**
+   * The logger that generated the message.
+   */
+  @SuppressWarnings("MemberName")
+  public final int logger;
+
+  /**
+   * Log level of the message.
+   */
+  @SuppressWarnings("MemberName")
+  public final int level;
+
+  /**
+   * The filename of the source file that generated the message.
+   */
+  @SuppressWarnings("MemberName")
+  public final String filename;
+
+  /**
+   * The line number in the source file that generated the message.
+   */
+  @SuppressWarnings("MemberName")
+  public final int line;
+
+  /**
+   * The message.
+   */
+  @SuppressWarnings("MemberName")
+  public final String message;
+
+  /** Constructor.
+   * This should generally only be used internally to NetworkTables.
+   *
+   * @param inst Instance
+   * @param logger Logger
+   * @param level Log level
+   * @param filename Filename
+   * @param line Line number
+   * @param message Message
+   */
+  public LogMessage(NetworkTableInstance inst, int logger, int level, String filename, int line,
+                    String message) {
+    this.m_inst = inst;
+    this.logger = logger;
+    this.level = level;
+    this.filename = filename;
+    this.line = line;
+    this.message = message;
+  }
+
+  private final NetworkTableInstance m_inst;
+
+  NetworkTableInstance getInstance() {
+    return m_inst;
+  }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTable.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTable.java
new file mode 100644
index 0000000..1cd21bf
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTable.java
@@ -0,0 +1,423 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Consumer;
+
+/**
+ * A network table that knows its subtable path.
+ */
+public final class NetworkTable {
+  /**
+   * The path separator for sub-tables and keys.
+   */
+  public static final char PATH_SEPARATOR = '/';
+
+  private final String m_path;
+  private final String m_pathWithSep;
+  private final NetworkTableInstance m_inst;
+
+  /**
+   * Gets the "base name" of a key. For example, "/foo/bar" becomes "bar".
+   * If the key has a trailing slash, returns an empty string.
+   *
+   * @param key key
+   * @return base name
+   */
+  public static String basenameKey(String key) {
+    final int slash = key.lastIndexOf(PATH_SEPARATOR);
+    if (slash == -1) {
+      return key;
+    }
+    return key.substring(slash + 1);
+  }
+
+  /**
+   * Normalizes an network table key to contain no consecutive slashes and
+   * optionally start with a leading slash. For example:
+   *
+   * <pre><code>
+   * normalizeKey("/foo/bar", true)  == "/foo/bar"
+   * normalizeKey("foo/bar", true)   == "/foo/bar"
+   * normalizeKey("/foo/bar", false) == "foo/bar"
+   * normalizeKey("foo//bar", false) == "foo/bar"
+   * </code></pre>
+   *
+   * @param key              the key to normalize
+   * @param withLeadingSlash whether or not the normalized key should begin
+   *                         with a leading slash
+   * @return normalized key
+   */
+  public static String normalizeKey(String key, boolean withLeadingSlash) {
+    String normalized;
+    if (withLeadingSlash) {
+      normalized = PATH_SEPARATOR + key;
+    } else {
+      normalized = key;
+    }
+    normalized = normalized.replaceAll(PATH_SEPARATOR + "{2,}", String.valueOf(PATH_SEPARATOR));
+
+    if (!withLeadingSlash && normalized.charAt(0) == PATH_SEPARATOR) {
+      // remove leading slash, if present
+      normalized = normalized.substring(1);
+    }
+    return normalized;
+  }
+
+  /**
+   * Normalizes a network table key to start with exactly one leading slash
+   * ("/") and contain no consecutive slashes. For example,
+   * {@code "//foo/bar/"} becomes {@code "/foo/bar/"} and
+   * {@code "///a/b/c"} becomes {@code "/a/b/c"}.
+   *
+   * <p>This is equivalent to {@code normalizeKey(key, true)}
+   *
+   * @param key the key to normalize
+   * @return normalized key
+   */
+  public static String normalizeKey(String key) {
+    return normalizeKey(key, true);
+  }
+
+  /**
+   * Gets a list of the names of all the super tables of a given key. For
+   * example, the key "/foo/bar/baz" has a hierarchy of "/", "/foo",
+   * "/foo/bar", and "/foo/bar/baz".
+   *
+   * @param key the key
+   * @return List of super tables
+   */
+  public static List<String> getHierarchy(String key) {
+    final String normal = normalizeKey(key, true);
+    List<String> hierarchy = new ArrayList<>();
+    if (normal.length() == 1) {
+      hierarchy.add(normal);
+      return hierarchy;
+    }
+    for (int i = 1; ; i = normal.indexOf(PATH_SEPARATOR, i + 1)) {
+      if (i == -1) {
+        // add the full key
+        hierarchy.add(normal);
+        break;
+      } else {
+        hierarchy.add(normal.substring(0, i));
+      }
+    }
+    return hierarchy;
+  }
+
+  /**
+   * Constructor.  Use NetworkTableInstance.getTable() or getSubTable() instead.
+   */
+  NetworkTable(NetworkTableInstance inst, String path) {
+    m_path = path;
+    m_pathWithSep = path + PATH_SEPARATOR;
+    m_inst = inst;
+  }
+
+  /**
+   * Gets the instance for the table.
+   *
+   * @return Instance
+   */
+  public NetworkTableInstance getInstance() {
+    return m_inst;
+  }
+
+  @Override
+  public String toString() {
+    return "NetworkTable: " + m_path;
+  }
+
+  private final ConcurrentMap<String, NetworkTableEntry> m_entries = new ConcurrentHashMap<>();
+
+  /**
+   * Gets the entry for a sub key.
+   *
+   * @param key the key name
+   * @return Network table entry.
+   */
+  public NetworkTableEntry getEntry(String key) {
+    NetworkTableEntry entry = m_entries.get(key);
+    if (entry == null) {
+      entry = m_inst.getEntry(m_pathWithSep + key);
+      m_entries.putIfAbsent(key, entry);
+    }
+    return entry;
+  }
+
+  /**
+   * Listen to keys only within this table.
+   *
+   * @param listener    listener to add
+   * @param flags       {@link EntryListenerFlags} bitmask
+   * @return Listener handle
+   */
+  public int addEntryListener(TableEntryListener listener, int flags) {
+    final int prefixLen = m_path.length() + 1;
+    return m_inst.addEntryListener(m_pathWithSep, event -> {
+      String relativeKey = event.name.substring(prefixLen);
+      if (relativeKey.indexOf(PATH_SEPARATOR) != -1) {
+        // part of a sub table
+        return;
+      }
+      listener.valueChanged(this, relativeKey, event.getEntry(), event.value, event.flags);
+    }, flags);
+  }
+
+  /**
+   * Listen to a single key.
+   *
+   * @param key         the key name
+   * @param listener    listener to add
+   * @param flags       {@link EntryListenerFlags} bitmask
+   * @return Listener handle
+   */
+  public int addEntryListener(String key, TableEntryListener listener, int flags) {
+    final NetworkTableEntry entry = getEntry(key);
+    return m_inst.addEntryListener(entry,
+        event -> listener.valueChanged(this, key, entry, event.value, event.flags), flags);
+  }
+
+  /**
+   * Remove an entry listener.
+   *
+   * @param listener    listener handle
+   */
+  public void removeEntryListener(int listener) {
+    m_inst.removeEntryListener(listener);
+  }
+
+  /**
+   * Listen for sub-table creation.
+   * This calls the listener once for each newly created sub-table.
+   * It immediately calls the listener for any existing sub-tables.
+   *
+   * @param listener        listener to add
+   * @param localNotify     notify local changes as well as remote
+   * @return Listener handle
+   */
+  public int addSubTableListener(TableListener listener, boolean localNotify) {
+    int flags = EntryListenerFlags.kNew | EntryListenerFlags.kImmediate;
+    if (localNotify) {
+      flags |= EntryListenerFlags.kLocal;
+    }
+
+    final int prefixLen = m_path.length() + 1;
+    final NetworkTable parent = this;
+
+    return m_inst.addEntryListener(m_pathWithSep, new Consumer<EntryNotification>() {
+      final Set<String> m_notifiedTables = new HashSet<>();
+
+      @Override
+      public void accept(EntryNotification event) {
+        String relativeKey = event.name.substring(prefixLen);
+        int endSubTable = relativeKey.indexOf(PATH_SEPARATOR);
+        if (endSubTable == -1) {
+          return;
+        }
+        String subTableKey = relativeKey.substring(0, endSubTable);
+        if (m_notifiedTables.contains(subTableKey)) {
+          return;
+        }
+        m_notifiedTables.add(subTableKey);
+        listener.tableCreated(parent, subTableKey, parent.getSubTable(subTableKey));
+      }
+    }, flags);
+  }
+
+  /**
+   * Remove a sub-table listener.
+   *
+   * @param listener    listener handle
+   */
+  public void removeTableListener(int listener) {
+    m_inst.removeEntryListener(listener);
+  }
+
+  /**
+   * Returns the table at the specified key. If there is no table at the
+   * specified key, it will create a new table
+   *
+   * @param key the name of the table relative to this one
+   * @return a sub table relative to this one
+   */
+  public NetworkTable getSubTable(String key) {
+    return new NetworkTable(m_inst, m_pathWithSep + key);
+  }
+
+  /**
+   * Checks the table and tells if it contains the specified key.
+   *
+   * @param key the key to search for
+   * @return true if the table as a value assigned to the given key
+   */
+  public boolean containsKey(String key) {
+    return !("".equals(key)) && getEntry(key).exists();
+  }
+
+  /**
+   * Checks the table and tells if it contains the specified sub table.
+   *
+   * @param key the key to search for
+   * @return true if there is a subtable with the key which contains at least one key/subtable of
+   *     its own
+   */
+  public boolean containsSubTable(String key) {
+    int[] handles = NetworkTablesJNI.getEntries(m_inst.getHandle(),
+        m_pathWithSep + key + PATH_SEPARATOR, 0);
+    return handles.length != 0;
+  }
+
+  /**
+   * Gets all keys in the table (not including sub-tables).
+   *
+   * @param types bitmask of types; 0 is treated as a "don't care".
+   * @return keys currently in the table
+   */
+  public Set<String> getKeys(int types) {
+    Set<String> keys = new HashSet<>();
+    int prefixLen = m_path.length() + 1;
+    for (EntryInfo info : m_inst.getEntryInfo(m_pathWithSep, types)) {
+      String relativeKey = info.name.substring(prefixLen);
+      if (relativeKey.indexOf(PATH_SEPARATOR) != -1) {
+        continue;
+      }
+      keys.add(relativeKey);
+      // populate entries as we go
+      if (m_entries.get(relativeKey) == null) {
+        m_entries.putIfAbsent(relativeKey, new NetworkTableEntry(m_inst, info.entry));
+      }
+    }
+    return keys;
+  }
+
+  /**
+   * Gets all keys in the table (not including sub-tables).
+   *
+   * @return keys currently in the table
+   */
+  public Set<String> getKeys() {
+    return getKeys(0);
+  }
+
+  /**
+   * Gets the names of all subtables in the table.
+   *
+   * @return subtables currently in the table
+   */
+  public Set<String> getSubTables() {
+    Set<String> keys = new HashSet<>();
+    int prefixLen = m_path.length() + 1;
+    for (EntryInfo info : m_inst.getEntryInfo(m_pathWithSep, 0)) {
+      String relativeKey = info.name.substring(prefixLen);
+      int endSubTable = relativeKey.indexOf(PATH_SEPARATOR);
+      if (endSubTable == -1) {
+        continue;
+      }
+      keys.add(relativeKey.substring(0, endSubTable));
+    }
+    return keys;
+  }
+
+  /**
+   * Deletes the specified key in this table. The key can
+   * not be null.
+   *
+   * @param key the key name
+   */
+  public void delete(String key) {
+    getEntry(key).delete();
+  }
+
+  /**
+   * Put a value in the table.
+   *
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  boolean putValue(String key, NetworkTableValue value) {
+    return getEntry(key).setValue(value);
+  }
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   *
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  boolean setDefaultValue(String key, NetworkTableValue defaultValue) {
+    return getEntry(key).setDefaultValue(defaultValue);
+  }
+
+  /**
+   * Gets the value associated with a key as an object.
+   *
+   * @param key the key of the value to look up
+   * @return the value associated with the given key, or nullptr if the key does not exist
+   */
+  NetworkTableValue getValue(String key) {
+    return getEntry(key).getValue();
+  }
+
+  /**
+   * Get the path of the NetworkTable.
+   */
+  public String getPath() {
+    return m_path;
+  }
+
+  /**
+   * Save table values to a file.  The file format used is identical to
+   * that used for SavePersistent.
+   *
+   * @param filename  filename
+   * @throws PersistentException if error saving file
+   */
+  public void saveEntries(String filename) throws PersistentException {
+    m_inst.saveEntries(filename, m_pathWithSep);
+  }
+
+  /**
+   * Load table values from a file.  The file format used is identical to
+   * that used for SavePersistent / LoadPersistent.
+   *
+   * @param filename  filename
+   * @return List of warnings (errors result in an exception instead)
+   * @throws PersistentException if error saving file
+   */
+  public String[] loadEntries(String filename) throws PersistentException {
+    return m_inst.loadEntries(filename, m_pathWithSep);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other == this) {
+      return true;
+    }
+    if (!(other instanceof NetworkTable)) {
+      return false;
+    }
+    NetworkTable ntOther = (NetworkTable) other;
+    return m_inst.equals(ntOther.m_inst) && m_path.equals(ntOther.m_path);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(m_inst, m_path);
+  }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEntry.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEntry.java
new file mode 100644
index 0000000..63ed984
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEntry.java
@@ -0,0 +1,875 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+import java.nio.ByteBuffer;
+import java.util.function.Consumer;
+
+/**
+ * NetworkTables Entry.
+ */
+public final class NetworkTableEntry {
+  /**
+   * Flag values (as returned by {@link #getFlags()}).
+   */
+  public static final int kPersistent = 0x01;
+
+  /**
+   * Construct from native handle.
+   *
+   * @param inst Instance
+   * @param handle Native handle
+   */
+  public NetworkTableEntry(NetworkTableInstance inst, int handle) {
+    m_inst = inst;
+    m_handle = handle;
+  }
+
+  /**
+   * Determines if the native handle is valid.
+   *
+   * @return True if the native handle is valid, false otherwise.
+   */
+  public boolean isValid() {
+    return m_handle != 0;
+  }
+
+  /**
+   * Gets the native handle for the entry.
+   *
+   * @return Native handle
+   */
+  public int getHandle() {
+    return m_handle;
+  }
+
+  /**
+   * Gets the instance for the entry.
+   *
+   * @return Instance
+   */
+  public NetworkTableInstance getInstance() {
+    return m_inst;
+  }
+
+  /**
+   * Determines if the entry currently exists.
+   *
+   * @return True if the entry exists, false otherwise.
+   */
+  public boolean exists() {
+    return NetworkTablesJNI.getType(m_handle) != 0;
+  }
+
+  /**
+   * Gets the name of the entry (the key).
+   *
+   * @return the entry's name
+   */
+  public String getName() {
+    return NetworkTablesJNI.getEntryName(m_handle);
+  }
+
+  /**
+   * Gets the type of the entry.
+   *
+   * @return the entry's type
+   */
+  public NetworkTableType getType() {
+    return NetworkTableType.getFromInt(NetworkTablesJNI.getType(m_handle));
+  }
+
+  /**
+   * Returns the flags.
+   *
+   * @return the flags (bitmask)
+   */
+  public int getFlags() {
+    return NetworkTablesJNI.getEntryFlags(m_handle);
+  }
+
+  /**
+   * Gets the last time the entry's value was changed.
+   *
+   * @return Entry last change time
+   */
+  public long getLastChange() {
+    return NetworkTablesJNI.getEntryLastChange(m_handle);
+  }
+
+  /**
+   * Gets combined information about the entry.
+   *
+   * @return Entry information
+   */
+  public EntryInfo getInfo() {
+    return NetworkTablesJNI.getEntryInfoHandle(m_inst, m_handle);
+  }
+
+  /**
+   * Gets the entry's value.
+   * Returns a value with type NetworkTableType.kUnassigned if the value
+   * does not exist.
+   *
+   * @return the entry's value
+   */
+  public NetworkTableValue getValue() {
+    return NetworkTablesJNI.getValue(m_handle);
+  }
+
+  /**
+   * Gets the entry's value as a boolean. If the entry does not exist or is of
+   * different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public boolean getBoolean(boolean defaultValue) {
+    return NetworkTablesJNI.getBoolean(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a double. If the entry does not exist or is of
+   * different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public double getDouble(double defaultValue) {
+    return NetworkTablesJNI.getDouble(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a double. If the entry does not exist or is of
+   * different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public Number getNumber(Number defaultValue) {
+    return NetworkTablesJNI.getDouble(m_handle, defaultValue.doubleValue());
+  }
+
+  /**
+   * Gets the entry's value as a string. If the entry does not exist or is of
+   * different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public String getString(String defaultValue) {
+    return NetworkTablesJNI.getString(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a raw value (byte array). If the entry does not
+   * exist or is of different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public byte[] getRaw(byte[] defaultValue) {
+    return NetworkTablesJNI.getRaw(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist
+   * or is of different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public boolean[] getBooleanArray(boolean[] defaultValue) {
+    return NetworkTablesJNI.getBooleanArray(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist
+   * or is of different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public Boolean[] getBooleanArray(Boolean[] defaultValue) {
+    return NetworkTableValue.fromNative(NetworkTablesJNI.getBooleanArray(m_handle,
+        NetworkTableValue.toNative(defaultValue)));
+  }
+
+  /**
+   * Gets the entry's value as a double array. If the entry does not exist
+   * or is of different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public double[] getDoubleArray(double[] defaultValue) {
+    return NetworkTablesJNI.getDoubleArray(m_handle, defaultValue);
+  }
+
+  /**
+   * Gets the entry's value as a double array. If the entry does not exist
+   * or is of different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public Double[] getDoubleArray(Double[] defaultValue) {
+    return NetworkTableValue.fromNative(NetworkTablesJNI.getDoubleArray(m_handle,
+        NetworkTableValue.toNative(defaultValue)));
+  }
+
+  /**
+   * Gets the entry's value as a double array. If the entry does not exist
+   * or is of different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public Number[] getNumberArray(Number[] defaultValue) {
+    return NetworkTableValue.fromNative(NetworkTablesJNI.getDoubleArray(m_handle,
+        NetworkTableValue.toNative(defaultValue)));
+  }
+
+  /**
+   * Gets the entry's value as a string array. If the entry does not exist
+   * or is of different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  public String[] getStringArray(String[] defaultValue) {
+    return NetworkTablesJNI.getStringArray(m_handle, defaultValue);
+  }
+
+  /**
+   * Checks if a data value is of a type that can be placed in a NetworkTable entry.
+   *
+   * @param data the data to check
+   * @return true if the data can be placed in an entry, false if it cannot
+   */
+  public static boolean isValidDataType(Object data) {
+    return data instanceof Number
+        || data instanceof Boolean
+        || data instanceof String
+        || data instanceof double[]
+        || data instanceof Double[]
+        || data instanceof Number[]
+        || data instanceof boolean[]
+        || data instanceof Boolean[]
+        || data instanceof String[]
+        || data instanceof byte[]
+        || data instanceof Byte[];
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   * @throws IllegalArgumentException if the value is not a known type
+   */
+  public boolean setDefaultValue(Object defaultValue) {
+    if (defaultValue instanceof NetworkTableValue) {
+      long time = ((NetworkTableValue) defaultValue).getTime();
+      Object otherValue = ((NetworkTableValue) defaultValue).getValue();
+      switch (((NetworkTableValue) defaultValue).getType()) {
+        case kBoolean:
+          return NetworkTablesJNI.setDefaultBoolean(m_handle, time,
+              ((Boolean) otherValue).booleanValue());
+        case kDouble:
+          return NetworkTablesJNI.setDefaultDouble(m_handle, time,
+              ((Number) otherValue).doubleValue());
+        case kString:
+          return NetworkTablesJNI.setDefaultString(m_handle, time, (String) otherValue);
+        case kRaw:
+          return NetworkTablesJNI.setDefaultRaw(m_handle, time, (byte[]) otherValue);
+        case kBooleanArray:
+          return NetworkTablesJNI.setDefaultBooleanArray(m_handle, time, (boolean[]) otherValue);
+        case kDoubleArray:
+          return NetworkTablesJNI.setDefaultDoubleArray(m_handle, time, (double[]) otherValue);
+        case kStringArray:
+          return NetworkTablesJNI.setDefaultStringArray(m_handle, time, (String[]) otherValue);
+        case kRpc:
+          // TODO
+        default:
+          return true;
+      }
+    } else if (defaultValue instanceof Boolean) {
+      return setDefaultBoolean((Boolean) defaultValue);
+    } else if (defaultValue instanceof Number) {
+      return setDefaultNumber((Number) defaultValue);
+    } else if (defaultValue instanceof String) {
+      return setDefaultString((String) defaultValue);
+    } else if (defaultValue instanceof byte[])  {
+      return setDefaultRaw((byte[]) defaultValue);
+    } else if (defaultValue instanceof boolean[])  {
+      return setDefaultBooleanArray((boolean[]) defaultValue);
+    } else if (defaultValue instanceof double[])  {
+      return setDefaultDoubleArray((double[]) defaultValue);
+    } else if (defaultValue instanceof Boolean[])  {
+      return setDefaultBooleanArray((Boolean[]) defaultValue);
+    } else if (defaultValue instanceof Number[])  {
+      return setDefaultNumberArray((Number[]) defaultValue);
+    } else if (defaultValue instanceof String[])  {
+      return setDefaultStringArray((String[]) defaultValue);
+    } else {
+      throw new IllegalArgumentException("Value of type " + defaultValue.getClass().getName()
+        + " cannot be put into a table");
+    }
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultBoolean(boolean defaultValue) {
+    return NetworkTablesJNI.setDefaultBoolean(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultDouble(double defaultValue) {
+    return NetworkTablesJNI.setDefaultDouble(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultNumber(Number defaultValue) {
+    return NetworkTablesJNI.setDefaultDouble(m_handle, 0, defaultValue.doubleValue());
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultString(String defaultValue) {
+    return NetworkTablesJNI.setDefaultString(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultRaw(byte[] defaultValue) {
+    return NetworkTablesJNI.setDefaultRaw(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultBooleanArray(boolean[] defaultValue) {
+    return NetworkTablesJNI.setDefaultBooleanArray(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultBooleanArray(Boolean[] defaultValue) {
+    return NetworkTablesJNI.setDefaultBooleanArray(m_handle,
+        0, NetworkTableValue.toNative(defaultValue));
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultDoubleArray(double[] defaultValue) {
+    return NetworkTablesJNI.setDefaultDoubleArray(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultNumberArray(Number[] defaultValue) {
+    return NetworkTablesJNI.setDefaultDoubleArray(m_handle,
+        0, NetworkTableValue.toNative(defaultValue));
+  }
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDefaultStringArray(String[] defaultValue) {
+    return NetworkTablesJNI.setDefaultStringArray(m_handle, 0, defaultValue);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   * @throws IllegalArgumentException if the value is not a known type
+   */
+  public boolean setValue(Object value) {
+    if (value instanceof NetworkTableValue) {
+      long time = ((NetworkTableValue) value).getTime();
+      Object otherValue = ((NetworkTableValue) value).getValue();
+      switch (((NetworkTableValue) value).getType()) {
+        case kBoolean:
+          return NetworkTablesJNI.setBoolean(m_handle, time, ((Boolean) otherValue).booleanValue(),
+              false);
+        case kDouble:
+          return NetworkTablesJNI.setDouble(m_handle, time, ((Number) otherValue).doubleValue(),
+              false);
+        case kString:
+          return NetworkTablesJNI.setString(m_handle, time, (String) otherValue, false);
+        case kRaw:
+          return NetworkTablesJNI.setRaw(m_handle, time, (byte[]) otherValue, false);
+        case kBooleanArray:
+          return NetworkTablesJNI.setBooleanArray(m_handle, time, (boolean[]) otherValue, false);
+        case kDoubleArray:
+          return NetworkTablesJNI.setDoubleArray(m_handle, time, (double[]) otherValue, false);
+        case kStringArray:
+          return NetworkTablesJNI.setStringArray(m_handle, time, (String[]) otherValue, false);
+        case kRpc:
+          // TODO
+        default:
+          return true;
+      }
+    } else if (value instanceof Boolean) {
+      return setBoolean((Boolean) value);
+    } else if (value instanceof Number) {
+      return setNumber((Number) value);
+    } else if (value instanceof String) {
+      return setString((String) value);
+    } else if (value instanceof byte[]) {
+      return setRaw((byte[]) value);
+    } else if (value instanceof boolean[]) {
+      return setBooleanArray((boolean[]) value);
+    } else if (value instanceof double[]) {
+      return setDoubleArray((double[]) value);
+    } else if (value instanceof Boolean[]) {
+      return setBooleanArray((Boolean[]) value);
+    } else if (value instanceof Number[]) {
+      return setNumberArray((Number[]) value);
+    } else if (value instanceof String[]) {
+      return setStringArray((String[]) value);
+    } else {
+      throw new IllegalArgumentException("Value of type " + value.getClass().getName()
+        + " cannot be put into a table");
+    }
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setBoolean(boolean value) {
+    return NetworkTablesJNI.setBoolean(m_handle, 0, value, false);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDouble(double value) {
+    return NetworkTablesJNI.setDouble(m_handle, 0, value, false);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setNumber(Number value) {
+    return NetworkTablesJNI.setDouble(m_handle, 0, value.doubleValue(), false);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setString(String value) {
+    return NetworkTablesJNI.setString(m_handle, 0, value, false);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setRaw(byte[] value) {
+    return NetworkTablesJNI.setRaw(m_handle, 0, value, false);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @param len the length of the value
+   * @return False if the entry exists with a different type
+   */
+  public boolean setRaw(ByteBuffer value, int len) {
+    if (!value.isDirect()) {
+      throw new IllegalArgumentException("must be a direct buffer");
+    }
+    if (value.capacity() < len) {
+      throw new IllegalArgumentException("buffer is too small, must be at least " + len);
+    }
+    return NetworkTablesJNI.setRaw(m_handle, 0, value, len, false);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setBooleanArray(boolean[] value) {
+    return NetworkTablesJNI.setBooleanArray(m_handle, 0, value, false);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setBooleanArray(Boolean[] value) {
+    return NetworkTablesJNI.setBooleanArray(m_handle, 0, NetworkTableValue.toNative(value), false);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setDoubleArray(double[] value) {
+    return NetworkTablesJNI.setDoubleArray(m_handle, 0, value, false);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setNumberArray(Number[] value) {
+    return NetworkTablesJNI.setDoubleArray(m_handle, 0, NetworkTableValue.toNative(value), false);
+  }
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  public boolean setStringArray(String[] value) {
+    return NetworkTablesJNI.setStringArray(m_handle, 0, value, false);
+  }
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   * @throws IllegalArgumentException if the value is not a known type
+   */
+  public void forceSetValue(Object value) {
+    if (value instanceof NetworkTableValue) {
+      long time = ((NetworkTableValue) value).getTime();
+      Object otherValue = ((NetworkTableValue) value).getValue();
+      switch (((NetworkTableValue) value).getType()) {
+        case kBoolean:
+          NetworkTablesJNI.setBoolean(m_handle, time, ((Boolean) otherValue).booleanValue(), true);
+          return;
+        case kDouble:
+          NetworkTablesJNI.setDouble(m_handle, time, ((Number) otherValue).doubleValue(), true);
+          return;
+        case kString:
+          NetworkTablesJNI.setString(m_handle, time, (String) otherValue, true);
+          return;
+        case kRaw:
+          NetworkTablesJNI.setRaw(m_handle, time, (byte[]) otherValue, true);
+          return;
+        case kBooleanArray:
+          NetworkTablesJNI.setBooleanArray(m_handle, time, (boolean[]) otherValue, true);
+          return;
+        case kDoubleArray:
+          NetworkTablesJNI.setDoubleArray(m_handle, time, (double[]) otherValue, true);
+          return;
+        case kStringArray:
+          NetworkTablesJNI.setStringArray(m_handle, time, (String[]) otherValue, true);
+          return;
+        case kRpc:
+          // TODO
+        default:
+          return;
+      }
+    } else if (value instanceof Boolean) {
+      forceSetBoolean((Boolean) value);
+    } else if (value instanceof Number) {
+      forceSetNumber((Number) value);
+    } else if (value instanceof String) {
+      forceSetString((String) value);
+    } else if (value instanceof byte[]) {
+      forceSetRaw((byte[]) value);
+    } else if (value instanceof boolean[]) {
+      forceSetBooleanArray((boolean[]) value);
+    } else if (value instanceof double[]) {
+      forceSetDoubleArray((double[]) value);
+    } else if (value instanceof Boolean[]) {
+      forceSetBooleanArray((Boolean[]) value);
+    } else if (value instanceof Number[]) {
+      forceSetNumberArray((Number[]) value);
+    } else if (value instanceof String[]) {
+      forceSetStringArray((String[]) value);
+    } else {
+      throw new IllegalArgumentException("Value of type " + value.getClass().getName()
+        + " cannot be put into a table");
+    }
+  }
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  public void forceSetBoolean(boolean value) {
+    NetworkTablesJNI.setBoolean(m_handle, 0, value, true);
+  }
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  public void forceSetDouble(double value) {
+    NetworkTablesJNI.setDouble(m_handle, 0, value, true);
+  }
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  public void forceSetNumber(Number value) {
+    NetworkTablesJNI.setDouble(m_handle, 0, value.doubleValue(), true);
+  }
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  public void forceSetString(String value) {
+    NetworkTablesJNI.setString(m_handle, 0, value, true);
+  }
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  public void forceSetRaw(byte[] value) {
+    NetworkTablesJNI.setRaw(m_handle, 0, value, true);
+  }
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  public void forceSetBooleanArray(boolean[] value) {
+    NetworkTablesJNI.setBooleanArray(m_handle, 0, value, true);
+  }
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  public void forceSetBooleanArray(Boolean[] value) {
+    NetworkTablesJNI.setBooleanArray(m_handle, 0, NetworkTableValue.toNative(value), true);
+  }
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  public void forceSetDoubleArray(double[] value) {
+    NetworkTablesJNI.setDoubleArray(m_handle, 0, value, true);
+  }
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  public void forceSetNumberArray(Number[] value) {
+    NetworkTablesJNI.setDoubleArray(m_handle, 0, NetworkTableValue.toNative(value), true);
+  }
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  public void forceSetStringArray(String[] value) {
+    NetworkTablesJNI.setStringArray(m_handle, 0, value, true);
+  }
+
+  /**
+   * Sets flags.
+   *
+   * @param flags the flags to set (bitmask)
+   */
+  public void setFlags(int flags) {
+    NetworkTablesJNI.setEntryFlags(m_handle, getFlags() | flags);
+  }
+
+  /**
+   * Clears flags.
+   *
+   * @param flags the flags to clear (bitmask)
+   */
+  public void clearFlags(int flags) {
+    NetworkTablesJNI.setEntryFlags(m_handle, getFlags() & ~flags);
+  }
+
+  /**
+   * Make value persistent through program restarts.
+   */
+  public void setPersistent() {
+    setFlags(kPersistent);
+  }
+
+  /**
+   * Stop making value persistent through program restarts.
+   */
+  public void clearPersistent() {
+    clearFlags(kPersistent);
+  }
+
+  /**
+   * Returns whether the value is persistent through program restarts.
+   *
+   * @return True if the value is persistent.
+   */
+  public boolean isPersistent() {
+    return (getFlags() & kPersistent) != 0;
+  }
+
+  /**
+   * Deletes the entry.
+   */
+  public void delete() {
+    NetworkTablesJNI.deleteEntry(m_handle);
+  }
+
+  /**
+   * Create a callback-based RPC entry point.  Only valid to use on the server.
+   * The callback function will be called when the RPC is called.
+   * This function creates RPC version 0 definitions (raw data in and out).
+   *
+   * @param callback  callback function
+   */
+  public void createRpc(Consumer<RpcAnswer> callback) {
+    m_inst.createRpc(this, callback);
+  }
+
+  /**
+   * Call a RPC function.  May be used on either the client or server.
+   * This function is non-blocking.  Either {@link RpcCall#getResult()} or
+   * {@link RpcCall#cancelResult()} must be called on the return value to either
+   * get or ignore the result of the call.
+   *
+   * @param params      parameter
+   * @return RPC call object.
+   */
+  public RpcCall callRpc(byte[] params) {
+    return new RpcCall(this, NetworkTablesJNI.callRpc(m_handle, params));
+  }
+
+  /**
+   * Add a listener for changes to the entry.
+   *
+   * @param listener the listener to add
+   * @param flags bitmask specifying desired notifications
+   * @return listener handle
+   */
+  public int addListener(Consumer<EntryNotification> listener, int flags) {
+    return m_inst.addEntryListener(this, listener, flags);
+  }
+
+  /**
+   * Remove a listener from receiving entry events.
+   *
+   * @param listener the listener to be removed
+   */
+  public void removeListener(int listener) {
+    m_inst.removeEntryListener(listener);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other == this) {
+      return true;
+    }
+    if (!(other instanceof NetworkTableEntry)) {
+      return false;
+    }
+
+    return m_handle == ((NetworkTableEntry) other).m_handle;
+  }
+
+  @Override
+  public int hashCode() {
+    return m_handle;
+  }
+
+  private NetworkTableInstance m_inst;
+  private int m_handle;
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableInstance.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableInstance.java
new file mode 100644
index 0000000..2564f70
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableInstance.java
@@ -0,0 +1,1165 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
+
+/**
+ * NetworkTables Instance.
+ *
+ * <p>Instances are completely independent from each other.  Table operations on
+ * one instance will not be visible to other instances unless the instances are
+ * connected via the network.  The main limitation on instances is that you
+ * cannot have two servers on the same network port.  The main utility of
+ * instances is for unit testing, but they can also enable one program to
+ * connect to two different NetworkTables networks.
+ *
+ * <p>The global "default" instance (as returned by {@link #getDefault()}) is
+ * always available, and is intended for the common case when there is only
+ * a single NetworkTables instance being used in the program.
+ *
+ * <p>Additional instances can be created with the {@link #create()} function.
+ * A reference must be kept to the NetworkTableInstance returned by this
+ * function to keep it from being garbage collected.
+ */
+public final class NetworkTableInstance implements AutoCloseable {
+  /**
+   * Client/server mode flag values (as returned by {@link #getNetworkMode()}).
+   * This is a bitmask.
+   */
+  public static final int kNetModeNone = 0x00;
+  public static final int kNetModeServer = 0x01;
+  public static final int kNetModeClient = 0x02;
+  public static final int kNetModeStarting = 0x04;
+  public static final int kNetModeFailure = 0x08;
+
+  /**
+   * The default port that network tables operates on.
+   */
+  public static final int kDefaultPort = 1735;
+
+  /**
+   * Construct from native handle.
+   *
+   * @param handle Native handle
+   */
+  private NetworkTableInstance(int handle) {
+    m_owned = false;
+    m_handle = handle;
+  }
+
+  @Deprecated
+  public void free() {
+    close();
+  }
+
+  /**
+   * Destroys the instance (if created by {@link #create()}).
+   */
+  @Override
+  public synchronized void close() {
+    if (m_owned && m_handle != 0) {
+      NetworkTablesJNI.destroyInstance(m_handle);
+    }
+  }
+
+  /**
+   * Determines if the native handle is valid.
+   *
+   * @return True if the native handle is valid, false otherwise.
+   */
+  public boolean isValid() {
+    return m_handle != 0;
+  }
+
+  /* The default instance. */
+  private static NetworkTableInstance s_defaultInstance;
+
+  /**
+   * Get global default instance.
+   *
+   * @return Global default instance
+   */
+  public static synchronized NetworkTableInstance getDefault() {
+    if (s_defaultInstance == null) {
+      s_defaultInstance = new NetworkTableInstance(NetworkTablesJNI.getDefaultInstance());
+    }
+    return s_defaultInstance;
+  }
+
+  /**
+   * Create an instance.
+   * Note: A reference to the returned instance must be retained to ensure the
+   * instance is not garbage collected.
+   *
+   * @return Newly created instance
+   */
+  public static NetworkTableInstance create() {
+    NetworkTableInstance inst = new NetworkTableInstance(NetworkTablesJNI.createInstance());
+    inst.m_owned = true;
+    return inst;
+  }
+
+  /**
+   * Gets the native handle for the entry.
+   *
+   * @return Native handle
+   */
+  public int getHandle() {
+    return m_handle;
+  }
+
+  /**
+   * Gets the entry for a key.
+   *
+   * @param name Key
+   * @return Network table entry.
+   */
+  public NetworkTableEntry getEntry(String name) {
+    return new NetworkTableEntry(this, NetworkTablesJNI.getEntry(m_handle, name));
+  }
+
+  /**
+   * Get entries starting with the given prefix.
+   * The results are optionally filtered by string prefix and entry type to
+   * only return a subset of all entries.
+   *
+   * @param prefix entry name required prefix; only entries whose name
+   *     starts with this string are returned
+   * @param types bitmask of types; 0 is treated as a "don't care"
+   * @return Array of entries.
+   */
+  public NetworkTableEntry[] getEntries(String prefix, int types) {
+    int[] handles = NetworkTablesJNI.getEntries(m_handle, prefix, types);
+    NetworkTableEntry[] entries = new NetworkTableEntry[handles.length];
+    for (int i = 0; i < handles.length; i++) {
+      entries[i] = new NetworkTableEntry(this, handles[i]);
+    }
+    return entries;
+  }
+
+  /**
+   * Get information about entries starting with the given prefix.
+   * The results are optionally filtered by string prefix and entry type to
+   * only return a subset of all entries.
+   *
+   * @param prefix entry name required prefix; only entries whose name
+   *     starts with this string are returned
+   * @param types bitmask of types; 0 is treated as a "don't care"
+   * @return Array of entry information.
+   */
+  public EntryInfo[] getEntryInfo(String prefix, int types) {
+    return NetworkTablesJNI.getEntryInfo(this, m_handle, prefix, types);
+  }
+
+  /* Cache of created tables. */
+  private final ConcurrentMap<String, NetworkTable> m_tables = new ConcurrentHashMap<>();
+
+  /**
+   * Gets the table with the specified key.
+   *
+   * @param key the key name
+   * @return The network table
+   */
+  public NetworkTable getTable(String key) {
+    // prepend leading / if not present
+    String theKey;
+    if (key.isEmpty() || key.equals("/")) {
+      theKey = "";
+    } else if (key.charAt(0) == NetworkTable.PATH_SEPARATOR) {
+      theKey = key;
+    } else {
+      theKey = NetworkTable.PATH_SEPARATOR + key;
+    }
+
+    // cache created tables
+    NetworkTable table = m_tables.get(theKey);
+    if (table == null) {
+      table = new NetworkTable(this, theKey);
+      NetworkTable oldTable = m_tables.putIfAbsent(theKey, table);
+      if (oldTable != null) {
+        table = oldTable;
+      }
+    }
+    return table;
+  }
+
+  /**
+   * Deletes ALL keys in ALL subtables (except persistent values).
+   * Use with caution!
+   */
+  public void deleteAllEntries() {
+    NetworkTablesJNI.deleteAllEntries(m_handle);
+  }
+
+  /*
+   * Callback Creation Functions
+   */
+
+  private static class EntryConsumer<T> {
+    final NetworkTableEntry m_entry;
+    final Consumer<T> m_consumer;
+
+    EntryConsumer(NetworkTableEntry entry, Consumer<T> consumer) {
+      m_entry = entry;
+      m_consumer = consumer;
+    }
+  }
+
+  private final ReentrantLock m_entryListenerLock = new ReentrantLock();
+  private final Map<Integer, EntryConsumer<EntryNotification>> m_entryListeners = new HashMap<>();
+  private Thread m_entryListenerThread;
+  private int m_entryListenerPoller;
+  private boolean m_entryListenerWaitQueue;
+  private final Condition m_entryListenerWaitQueueCond = m_entryListenerLock.newCondition();
+
+  private void startEntryListenerThread() {
+    m_entryListenerThread = new Thread(() -> {
+      boolean wasInterrupted = false;
+      while (!Thread.interrupted()) {
+        EntryNotification[] events;
+        try {
+          events = NetworkTablesJNI.pollEntryListener(this, m_entryListenerPoller);
+        } catch (InterruptedException ex) {
+          m_entryListenerLock.lock();
+          try {
+            if (m_entryListenerWaitQueue) {
+              m_entryListenerWaitQueue = false;
+              m_entryListenerWaitQueueCond.signalAll();
+              continue;
+            }
+          } finally {
+            m_entryListenerLock.unlock();
+          }
+          Thread.currentThread().interrupt();
+          // don't try to destroy poller, as its handle is likely no longer valid
+          wasInterrupted = true;
+          break;
+        }
+        for (EntryNotification event : events) {
+          EntryConsumer<EntryNotification> listener;
+          m_entryListenerLock.lock();
+          try {
+            listener = m_entryListeners.get(event.listener);
+          } finally {
+            m_entryListenerLock.unlock();
+          }
+          if (listener != null) {
+            event.m_entryObject = listener.m_entry;
+            try {
+              listener.m_consumer.accept(event);
+            } catch (Throwable throwable) {
+              System.err.println("Unhandled exception during entry listener callback: "
+                  + throwable.toString());
+              throwable.printStackTrace();
+            }
+          }
+        }
+      }
+      m_entryListenerLock.lock();
+      try {
+        if (!wasInterrupted) {
+          NetworkTablesJNI.destroyEntryListenerPoller(m_entryListenerPoller);
+        }
+        m_entryListenerPoller = 0;
+      } finally {
+        m_entryListenerLock.unlock();
+      }
+    }, "NTEntryListener");
+    m_entryListenerThread.setDaemon(true);
+    m_entryListenerThread.start();
+  }
+
+  /**
+   * Add a listener for all entries starting with a certain prefix.
+   *
+   * @param prefix            UTF-8 string prefix
+   * @param listener          listener to add
+   * @param flags             {@link EntryListenerFlags} bitmask
+   * @return Listener handle
+   */
+  public int addEntryListener(String prefix, Consumer<EntryNotification> listener, int flags) {
+    m_entryListenerLock.lock();
+    try {
+      if (m_entryListenerPoller == 0) {
+        m_entryListenerPoller = NetworkTablesJNI.createEntryListenerPoller(m_handle);
+        startEntryListenerThread();
+      }
+      int handle = NetworkTablesJNI.addPolledEntryListener(m_entryListenerPoller, prefix, flags);
+      m_entryListeners.put(handle, new EntryConsumer<>(null, listener));
+      return handle;
+    } finally {
+      m_entryListenerLock.unlock();
+    }
+  }
+
+  /**
+   * Add a listener for a particular entry.
+   *
+   * @param entry             the entry
+   * @param listener          listener to add
+   * @param flags             {@link EntryListenerFlags} bitmask
+   * @return Listener handle
+   */
+  public int addEntryListener(NetworkTableEntry entry,
+                              Consumer<EntryNotification> listener,
+                              int flags) {
+    if (!equals(entry.getInstance())) {
+      throw new IllegalArgumentException("entry does not belong to this instance");
+    }
+    m_entryListenerLock.lock();
+    try {
+      if (m_entryListenerPoller == 0) {
+        m_entryListenerPoller = NetworkTablesJNI.createEntryListenerPoller(m_handle);
+        startEntryListenerThread();
+      }
+      int handle = NetworkTablesJNI.addPolledEntryListener(m_entryListenerPoller, entry.getHandle(),
+          flags);
+      m_entryListeners.put(handle, new EntryConsumer<>(entry, listener));
+      return handle;
+    } finally {
+      m_entryListenerLock.unlock();
+    }
+  }
+
+  /**
+   * Remove an entry listener.
+   *
+   * @param listener Listener handle to remove
+   */
+  public void removeEntryListener(int listener) {
+    NetworkTablesJNI.removeEntryListener(listener);
+  }
+
+  /**
+   * Wait for the entry listener queue to be empty.  This is primarily useful
+   * for deterministic testing.  This blocks until either the entry listener
+   * queue is empty (e.g. there are no more events that need to be passed along
+   * to callbacks or poll queues) or the timeout expires.
+   *
+   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+   *                  or a negative value to block indefinitely
+   * @return False if timed out, otherwise true.
+  */
+  public boolean waitForEntryListenerQueue(double timeout) {
+    if (!NetworkTablesJNI.waitForEntryListenerQueue(m_handle, timeout)) {
+      return false;
+    }
+    m_entryListenerLock.lock();
+    try {
+      if (m_entryListenerPoller != 0) {
+        m_entryListenerWaitQueue = true;
+        NetworkTablesJNI.cancelPollEntryListener(m_entryListenerPoller);
+        while (m_entryListenerWaitQueue) {
+          try {
+            if (timeout < 0) {
+              m_entryListenerWaitQueueCond.await();
+            } else {
+              return m_entryListenerWaitQueueCond.await((long) (timeout * 1e9),
+                  TimeUnit.NANOSECONDS);
+            }
+          } catch (InterruptedException ex) {
+            Thread.currentThread().interrupt();
+            return true;
+          }
+        }
+      }
+    } finally {
+      m_entryListenerLock.unlock();
+    }
+    return true;
+  }
+
+  private final ReentrantLock m_connectionListenerLock = new ReentrantLock();
+  private final Map<Integer, Consumer<ConnectionNotification>> m_connectionListeners
+      = new HashMap<>();
+  private Thread m_connectionListenerThread;
+  private int m_connectionListenerPoller;
+  private boolean m_connectionListenerWaitQueue;
+  private final Condition m_connectionListenerWaitQueueCond
+      = m_connectionListenerLock.newCondition();
+
+  private void startConnectionListenerThread() {
+    m_connectionListenerThread = new Thread(() -> {
+      boolean wasInterrupted = false;
+      while (!Thread.interrupted()) {
+        ConnectionNotification[] events;
+        try {
+          events = NetworkTablesJNI.pollConnectionListener(this, m_connectionListenerPoller);
+        } catch (InterruptedException ex) {
+          m_connectionListenerLock.lock();
+          try {
+            if (m_connectionListenerWaitQueue) {
+              m_connectionListenerWaitQueue = false;
+              m_connectionListenerWaitQueueCond.signalAll();
+              continue;
+            }
+          } finally {
+            m_connectionListenerLock.unlock();
+          }
+          Thread.currentThread().interrupt();
+          // don't try to destroy poller, as its handle is likely no longer valid
+          wasInterrupted = true;
+          break;
+        }
+        for (ConnectionNotification event : events) {
+          Consumer<ConnectionNotification> listener;
+          m_connectionListenerLock.lock();
+          try {
+            listener = m_connectionListeners.get(event.listener);
+          } finally {
+            m_connectionListenerLock.unlock();
+          }
+          if (listener != null) {
+            try {
+              listener.accept(event);
+            } catch (Throwable throwable) {
+              System.err.println("Unhandled exception during connection listener callback: "
+                  + throwable.toString());
+              throwable.printStackTrace();
+            }
+          }
+        }
+      }
+      m_connectionListenerLock.lock();
+      try {
+        if (!wasInterrupted) {
+          NetworkTablesJNI.destroyConnectionListenerPoller(m_connectionListenerPoller);
+        }
+        m_connectionListenerPoller = 0;
+      } finally {
+        m_connectionListenerLock.unlock();
+      }
+    }, "NTConnectionListener");
+    m_connectionListenerThread.setDaemon(true);
+    m_connectionListenerThread.start();
+  }
+
+  /**
+   * Add a connection listener.
+   *
+   * @param listener Listener to add
+   * @param immediateNotify Notify listener of all existing connections
+   * @return Listener handle
+   */
+  public int addConnectionListener(Consumer<ConnectionNotification> listener,
+                                   boolean immediateNotify) {
+    m_connectionListenerLock.lock();
+    try {
+      if (m_connectionListenerPoller == 0) {
+        m_connectionListenerPoller = NetworkTablesJNI.createConnectionListenerPoller(m_handle);
+        startConnectionListenerThread();
+      }
+      int handle = NetworkTablesJNI.addPolledConnectionListener(m_connectionListenerPoller,
+          immediateNotify);
+      m_connectionListeners.put(handle, listener);
+      return handle;
+    } finally {
+      m_connectionListenerLock.unlock();
+    }
+  }
+
+  /**
+   * Remove a connection listener.
+   *
+   * @param listener Listener handle to remove
+   */
+  public void removeConnectionListener(int listener) {
+    m_connectionListenerLock.lock();
+    try {
+      m_connectionListeners.remove(listener);
+    } finally {
+      m_connectionListenerLock.unlock();
+    }
+    NetworkTablesJNI.removeConnectionListener(listener);
+  }
+
+  /**
+   * Wait for the connection listener queue to be empty.  This is primarily useful
+   * for deterministic testing.  This blocks until either the connection listener
+   * queue is empty (e.g. there are no more events that need to be passed along
+   * to callbacks or poll queues) or the timeout expires.
+   *
+   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+   *                  or a negative value to block indefinitely
+   * @return False if timed out, otherwise true.
+   */
+  public boolean waitForConnectionListenerQueue(double timeout) {
+    if (!NetworkTablesJNI.waitForConnectionListenerQueue(m_handle, timeout)) {
+      return false;
+    }
+    m_connectionListenerLock.lock();
+    try {
+      if (m_connectionListenerPoller != 0) {
+        m_connectionListenerWaitQueue = true;
+        NetworkTablesJNI.cancelPollConnectionListener(m_connectionListenerPoller);
+        while (m_connectionListenerWaitQueue) {
+          try {
+            if (timeout < 0) {
+              m_connectionListenerWaitQueueCond.await();
+            } else {
+              return m_connectionListenerWaitQueueCond.await((long) (timeout * 1e9),
+                  TimeUnit.NANOSECONDS);
+            }
+          } catch (InterruptedException ex) {
+            Thread.currentThread().interrupt();
+            return true;
+          }
+        }
+      }
+    } finally {
+      m_connectionListenerLock.unlock();
+    }
+    return true;
+  }
+
+  /*
+   * Remote Procedure Call Functions
+   */
+
+  private final ReentrantLock m_rpcCallLock = new ReentrantLock();
+  private final Map<Integer, EntryConsumer<RpcAnswer>> m_rpcCalls = new HashMap<>();
+  private Thread m_rpcCallThread;
+  private int m_rpcCallPoller;
+  private boolean m_rpcCallWaitQueue;
+  private final Condition m_rpcCallWaitQueueCond = m_rpcCallLock.newCondition();
+
+  private void startRpcCallThread() {
+    m_rpcCallThread = new Thread(() -> {
+      boolean wasInterrupted = false;
+      while (!Thread.interrupted()) {
+        RpcAnswer[] events;
+        try {
+          events = NetworkTablesJNI.pollRpc(this, m_rpcCallPoller);
+        } catch (InterruptedException ex) {
+          m_rpcCallLock.lock();
+          try {
+            if (m_rpcCallWaitQueue) {
+              m_rpcCallWaitQueue = false;
+              m_rpcCallWaitQueueCond.signalAll();
+              continue;
+            }
+          } finally {
+            m_rpcCallLock.unlock();
+          }
+          Thread.currentThread().interrupt();
+          // don't try to destroy poller, as its handle is likely no longer valid
+          wasInterrupted = true;
+          break;
+        }
+        for (RpcAnswer event : events) {
+          EntryConsumer<RpcAnswer> listener;
+          m_rpcCallLock.lock();
+          try {
+            listener = m_rpcCalls.get(event.entry);
+          } finally {
+            m_rpcCallLock.unlock();
+          }
+          if (listener != null) {
+            event.m_entryObject = listener.m_entry;
+            try {
+              listener.m_consumer.accept(event);
+            } catch (Throwable throwable) {
+              System.err.println("Unhandled exception during RPC callback: "
+                  + throwable.toString());
+              throwable.printStackTrace();
+            }
+            event.finish();
+          }
+        }
+      }
+      m_rpcCallLock.lock();
+      try {
+        if (!wasInterrupted) {
+          NetworkTablesJNI.destroyRpcCallPoller(m_rpcCallPoller);
+        }
+        m_rpcCallPoller = 0;
+      } finally {
+        m_rpcCallLock.unlock();
+      }
+    }, "NTRpcCall");
+    m_rpcCallThread.setDaemon(true);
+    m_rpcCallThread.start();
+  }
+
+  private static final byte[] rev0def = new byte[] {0};
+
+  /**
+   * Create a callback-based RPC entry point.  Only valid to use on the server.
+   * The callback function will be called when the RPC is called.
+   * This function creates RPC version 0 definitions (raw data in and out).
+   *
+   * @param entry     the entry
+   * @param callback  callback function
+   */
+  public void createRpc(NetworkTableEntry entry, Consumer<RpcAnswer> callback) {
+    m_rpcCallLock.lock();
+    try {
+      if (m_rpcCallPoller == 0) {
+        m_rpcCallPoller = NetworkTablesJNI.createRpcCallPoller(m_handle);
+        startRpcCallThread();
+      }
+      NetworkTablesJNI.createPolledRpc(entry.getHandle(), rev0def, m_rpcCallPoller);
+      m_rpcCalls.put(entry.getHandle(), new EntryConsumer<>(entry, callback));
+    } finally {
+      m_rpcCallLock.unlock();
+    }
+  }
+
+  /**
+   * Wait for the incoming RPC call queue to be empty.  This is primarily useful
+   * for deterministic testing.  This blocks until either the RPC call
+   * queue is empty (e.g. there are no more events that need to be passed along
+   * to callbacks or poll queues) or the timeout expires.
+   *
+   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+   *                  or a negative value to block indefinitely
+   * @return False if timed out, otherwise true.
+   */
+  public boolean waitForRpcCallQueue(double timeout) {
+    if (!NetworkTablesJNI.waitForRpcCallQueue(m_handle, timeout)) {
+      return false;
+    }
+    m_rpcCallLock.lock();
+    try {
+      if (m_rpcCallPoller != 0) {
+        m_rpcCallWaitQueue = true;
+        NetworkTablesJNI.cancelPollRpc(m_rpcCallPoller);
+        while (m_rpcCallWaitQueue) {
+          try {
+            if (timeout < 0) {
+              m_rpcCallWaitQueueCond.await();
+            } else {
+              return m_rpcCallWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
+            }
+          } catch (InterruptedException ex) {
+            Thread.currentThread().interrupt();
+            return true;
+          }
+        }
+      }
+    } finally {
+      m_rpcCallLock.unlock();
+    }
+    return true;
+  }
+
+  /*
+   * Client/Server Functions
+   */
+
+  /**
+   * Set the network identity of this node.
+   * This is the name used during the initial connection handshake, and is
+   * visible through ConnectionInfo on the remote node.
+   *
+   * @param name      identity to advertise
+   */
+  public void setNetworkIdentity(String name) {
+    NetworkTablesJNI.setNetworkIdentity(m_handle, name);
+  }
+
+  /**
+   * Get the current network mode.
+   *
+   * @return Bitmask of NetworkMode.
+   */
+  public int getNetworkMode() {
+    return NetworkTablesJNI.getNetworkMode(m_handle);
+  }
+
+  /**
+   * Starts a server using the networktables.ini as the persistent file,
+   * using the default listening address and port.
+   */
+  public void startServer() {
+    startServer("networktables.ini");
+  }
+
+  /**
+   * Starts a server using the specified persistent filename, using the default
+   * listening address and port.
+   *
+   * @param persistFilename  the name of the persist file to use
+   */
+  public void startServer(String persistFilename) {
+    startServer(persistFilename, "");
+  }
+
+  /**
+   * Starts a server using the specified filename and listening address,
+   * using the default port.
+   *
+   * @param persistFilename  the name of the persist file to use
+   * @param listenAddress    the address to listen on, or empty to listen on any
+   *                         address
+   */
+  public void startServer(String persistFilename, String listenAddress) {
+    startServer(persistFilename, listenAddress, kDefaultPort);
+  }
+
+  /**
+   * Starts a server using the specified filename, listening address, and port.
+   *
+   * @param persistFilename  the name of the persist file to use
+   * @param listenAddress    the address to listen on, or empty to listen on any
+   *                         address
+   * @param port             port to communicate over
+   */
+  public void startServer(String persistFilename, String listenAddress, int port) {
+    NetworkTablesJNI.startServer(m_handle, persistFilename, listenAddress, port);
+  }
+
+  /**
+   * Stops the server if it is running.
+   */
+  public void stopServer() {
+    NetworkTablesJNI.stopServer(m_handle);
+  }
+
+  /**
+   * Starts a client.  Use SetServer to set the server name and port.
+   */
+  public void startClient() {
+    NetworkTablesJNI.startClient(m_handle);
+  }
+
+  /**
+   * Starts a client using the specified server and the default port.
+   *
+   * @param serverName  server name
+   */
+  public void startClient(String serverName) {
+    startClient(serverName, kDefaultPort);
+  }
+
+  /**
+   * Starts a client using the specified server and port.
+   *
+   * @param serverName  server name
+   * @param port        port to communicate over
+   */
+  public void startClient(String serverName, int port) {
+    NetworkTablesJNI.startClient(m_handle, serverName, port);
+  }
+
+  /**
+   * Starts a client using the specified servers and default port.  The
+   * client will attempt to connect to each server in round robin fashion.
+   *
+   * @param serverNames   array of server names
+   */
+  public void startClient(String[] serverNames) {
+    startClient(serverNames, kDefaultPort);
+  }
+
+  /**
+   * Starts a client using the specified servers and port number.  The
+   * client will attempt to connect to each server in round robin fashion.
+   *
+   * @param serverNames   array of server names
+   * @param port          port to communicate over
+   */
+  public void startClient(String[] serverNames, int port) {
+    int[] ports = new int[serverNames.length];
+    for (int i = 0; i < serverNames.length; i++) {
+      ports[i] = port;
+    }
+    startClient(serverNames, ports);
+  }
+
+  /**
+   * Starts a client using the specified (server, port) combinations.  The
+   * client will attempt to connect to each server in round robin fashion.
+   *
+   * @param serverNames   array of server names
+   * @param ports         array of port numbers
+   */
+  public void startClient(String[] serverNames, int[] ports) {
+    NetworkTablesJNI.startClient(m_handle, serverNames, ports);
+  }
+
+  /**
+   * Starts a client using commonly known robot addresses for the specified
+   * team using the default port number.
+   *
+   * @param team        team number
+   */
+  public void startClientTeam(int team) {
+    startClientTeam(team, kDefaultPort);
+  }
+
+  /**
+   * Starts a client using commonly known robot addresses for the specified
+   * team.
+   *
+   * @param team        team number
+   * @param port        port to communicate over
+   */
+  public void startClientTeam(int team, int port) {
+    NetworkTablesJNI.startClientTeam(m_handle, team, port);
+  }
+
+  /**
+   * Stops the client if it is running.
+   */
+  public void stopClient() {
+    NetworkTablesJNI.stopClient(m_handle);
+  }
+
+  /**
+   * Sets server address and port for client (without restarting client).
+   * Changes the port to the default port.
+   *
+   * @param serverName  server name
+   */
+  public void setServer(String serverName) {
+    setServer(serverName, kDefaultPort);
+  }
+
+  /**
+   * Sets server address and port for client (without restarting client).
+   *
+   * @param serverName  server name
+   * @param port        port to communicate over
+   */
+  public void setServer(String serverName, int port) {
+    NetworkTablesJNI.setServer(m_handle, serverName, port);
+  }
+
+  /**
+   * Sets server addresses and port for client (without restarting client).
+   * Changes the port to the default port.  The client will attempt to connect
+   * to each server in round robin fashion.
+   *
+   * @param serverNames   array of server names
+   */
+  public void setServer(String[] serverNames) {
+    setServer(serverNames, kDefaultPort);
+  }
+
+  /**
+   * Sets server addresses and port for client (without restarting client).
+   * The client will attempt to connect to each server in round robin fashion.
+   *
+   * @param serverNames   array of server names
+   * @param port          port to communicate over
+   */
+  public void setServer(String[] serverNames, int port) {
+    int[] ports = new int[serverNames.length];
+    for (int i = 0; i < serverNames.length; i++) {
+      ports[i] = port;
+    }
+    setServer(serverNames, ports);
+  }
+
+  /**
+   * Sets server addresses and ports for client (without restarting client).
+   * The client will attempt to connect to each server in round robin fashion.
+   *
+   * @param serverNames   array of server names
+   * @param ports         array of port numbers
+   */
+  public void setServer(String[] serverNames, int[] ports) {
+    NetworkTablesJNI.setServer(m_handle, serverNames, ports);
+  }
+
+  /**
+   * Sets server addresses and port for client (without restarting client).
+   * Changes the port to the default port.  The client will attempt to connect
+   * to each server in round robin fashion.
+   *
+   * @param team        team number
+   */
+  public void setServerTeam(int team) {
+    setServerTeam(team, kDefaultPort);
+  }
+
+  /**
+   * Sets server addresses and port for client (without restarting client).
+   * Connects using commonly known robot addresses for the specified team.
+   *
+   * @param team        team number
+   * @param port        port to communicate over
+   */
+  public void setServerTeam(int team, int port) {
+    NetworkTablesJNI.setServerTeam(m_handle, team, port);
+  }
+
+  /**
+   * Starts requesting server address from Driver Station.
+   * This connects to the Driver Station running on localhost to obtain the
+   * server IP address, and connects with the default port.
+   */
+  public void startDSClient() {
+    startDSClient(kDefaultPort);
+  }
+
+  /**
+   * Starts requesting server address from Driver Station.
+   * This connects to the Driver Station running on localhost to obtain the
+   * server IP address.
+   *
+   * @param port server port to use in combination with IP from DS
+   */
+  public void startDSClient(int port) {
+    NetworkTablesJNI.startDSClient(m_handle, port);
+  }
+
+  /**
+   * Stops requesting server address from Driver Station.
+   */
+  public void stopDSClient() {
+    NetworkTablesJNI.stopDSClient(m_handle);
+  }
+
+  /**
+   * Set the periodic update rate.
+   * Sets how frequently updates are sent to other nodes over the network.
+   *
+   * @param interval update interval in seconds (range 0.01 to 1.0)
+   */
+  public void setUpdateRate(double interval) {
+    NetworkTablesJNI.setUpdateRate(m_handle, interval);
+  }
+
+  /**
+   * Flushes all updated values immediately to the network.
+   * Note: This is rate-limited to protect the network from flooding.
+   * This is primarily useful for synchronizing network updates with
+   * user code.
+   */
+  public void flush() {
+    NetworkTablesJNI.flush(m_handle);
+  }
+
+  /**
+   * Gets information on the currently established network connections.
+   * If operating as a client, this will return either zero or one values.
+   *
+   * @return array of connection information
+   */
+  public ConnectionInfo[] getConnections() {
+    return NetworkTablesJNI.getConnections(m_handle);
+  }
+
+  /**
+   * Return whether or not the instance is connected to another node.
+   *
+   * @return True if connected.
+   */
+  public boolean isConnected() {
+    return NetworkTablesJNI.isConnected(m_handle);
+  }
+
+  /**
+   * Saves persistent keys to a file.  The server does this automatically.
+   *
+   * @param filename file name
+   * @throws PersistentException if error saving file
+   */
+  public void savePersistent(String filename) throws PersistentException {
+    NetworkTablesJNI.savePersistent(m_handle, filename);
+  }
+
+  /**
+   * Loads persistent keys from a file.  The server does this automatically.
+   *
+   * @param filename file name
+   * @return List of warnings (errors result in an exception instead)
+   * @throws PersistentException if error reading file
+   */
+  public String[] loadPersistent(String filename) throws PersistentException {
+    return NetworkTablesJNI.loadPersistent(m_handle, filename);
+  }
+
+  /**
+   * Save table values to a file.  The file format used is identical to
+   * that used for SavePersistent.
+   *
+   * @param filename  filename
+   * @param prefix    save only keys starting with this prefix
+   * @throws PersistentException if error saving file
+   */
+  public void saveEntries(String filename, String prefix) throws PersistentException {
+    NetworkTablesJNI.saveEntries(m_handle, filename, prefix);
+  }
+
+  /**
+   * Load table values from a file.  The file format used is identical to
+   * that used for SavePersistent / LoadPersistent.
+   *
+   * @param filename  filename
+   * @param prefix    load only keys starting with this prefix
+   * @return List of warnings (errors result in an exception instead)
+   * @throws PersistentException if error saving file
+   */
+  public String[] loadEntries(String filename, String prefix) throws PersistentException {
+    return NetworkTablesJNI.loadEntries(m_handle, filename, prefix);
+  }
+
+  private final ReentrantLock m_loggerLock = new ReentrantLock();
+  private final Map<Integer, Consumer<LogMessage>> m_loggers = new HashMap<>();
+  private Thread m_loggerThread;
+  private int m_loggerPoller;
+  private boolean m_loggerWaitQueue;
+  private final Condition m_loggerWaitQueueCond = m_loggerLock.newCondition();
+
+  private void startLogThread() {
+    m_loggerThread = new Thread(() -> {
+      boolean wasInterrupted = false;
+      while (!Thread.interrupted()) {
+        LogMessage[] events;
+        try {
+          events = NetworkTablesJNI.pollLogger(this, m_loggerPoller);
+        } catch (InterruptedException ex) {
+          Thread.currentThread().interrupt();
+          // don't try to destroy poller, as its handle is likely no longer valid
+          wasInterrupted = true;
+          break;
+        }
+        for (LogMessage event : events) {
+          Consumer<LogMessage> logger;
+          m_loggerLock.lock();
+          try {
+            logger = m_loggers.get(event.logger);
+          } finally {
+            m_loggerLock.unlock();
+          }
+          if (logger != null) {
+            try {
+              logger.accept(event);
+            } catch (Throwable throwable) {
+              System.err.println("Unhandled exception during logger callback: "
+                  + throwable.toString());
+              throwable.printStackTrace();
+            }
+          }
+        }
+      }
+      m_loggerLock.lock();
+      try {
+        if (!wasInterrupted) {
+          NetworkTablesJNI.destroyLoggerPoller(m_loggerPoller);
+        }
+        m_rpcCallPoller = 0;
+      } finally {
+        m_loggerLock.unlock();
+      }
+    }, "NTLogger");
+    m_loggerThread.setDaemon(true);
+    m_loggerThread.start();
+  }
+
+  /**
+   * Add logger callback function.  By default, log messages are sent to stderr;
+   * this function sends log messages with the specified levels to the provided
+   * callback function instead.  The callback function will only be called for
+   * log messages with level greater than or equal to minLevel and less than or
+   * equal to maxLevel; messages outside this range will be silently ignored.
+   *
+   * @param func        log callback function
+   * @param minLevel    minimum log level
+   * @param maxLevel    maximum log level
+   * @return Logger handle
+   */
+  public int addLogger(Consumer<LogMessage> func, int minLevel, int maxLevel) {
+    m_loggerLock.lock();
+    try {
+      if (m_loggerPoller == 0) {
+        m_loggerPoller = NetworkTablesJNI.createLoggerPoller(m_handle);
+        startLogThread();
+      }
+      int handle = NetworkTablesJNI.addPolledLogger(m_loggerPoller, minLevel, maxLevel);
+      m_loggers.put(handle, func);
+      return handle;
+    } finally {
+      m_loggerLock.unlock();
+    }
+  }
+
+  /**
+   * Remove a logger.
+   *
+   * @param logger Logger handle to remove
+   */
+  public void removeLogger(int logger) {
+    m_loggerLock.lock();
+    try {
+      m_loggers.remove(logger);
+    } finally {
+      m_loggerLock.unlock();
+    }
+    NetworkTablesJNI.removeLogger(logger);
+  }
+
+  /**
+   * Wait for the incoming log event queue to be empty.  This is primarily useful
+   * for deterministic testing.  This blocks until either the log event
+   * queue is empty (e.g. there are no more events that need to be passed along
+   * to callbacks or poll queues) or the timeout expires.
+   *
+   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+   *                  or a negative value to block indefinitely
+   * @return False if timed out, otherwise true.
+   */
+  public boolean waitForLoggerQueue(double timeout) {
+    if (!NetworkTablesJNI.waitForLoggerQueue(m_handle, timeout)) {
+      return false;
+    }
+    m_loggerLock.lock();
+    try {
+      if (m_loggerPoller != 0) {
+        m_loggerWaitQueue = true;
+        NetworkTablesJNI.cancelPollLogger(m_loggerPoller);
+        while (m_loggerWaitQueue) {
+          try {
+            if (timeout < 0) {
+              m_loggerWaitQueueCond.await();
+            } else {
+              return m_loggerWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
+            }
+          } catch (InterruptedException ex) {
+            Thread.currentThread().interrupt();
+            return true;
+          }
+        }
+      }
+    } finally {
+      m_loggerLock.unlock();
+    }
+    return true;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other == this) {
+      return true;
+    }
+    if (!(other instanceof NetworkTableInstance)) {
+      return false;
+    }
+
+    return m_handle == ((NetworkTableInstance) other).m_handle;
+  }
+
+  @Override
+  public int hashCode() {
+    return m_handle;
+  }
+
+  private boolean m_owned;
+  private int m_handle;
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableType.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableType.java
new file mode 100644
index 0000000..54a9f55
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableType.java
@@ -0,0 +1,54 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+/**
+ * Network table data types.
+ */
+public enum NetworkTableType {
+  kUnassigned(0),
+  kBoolean(0x01),
+  kDouble(0x02),
+  kString(0x04),
+  kRaw(0x08),
+  kBooleanArray(0x10),
+  kDoubleArray(0x20),
+  kStringArray(0x40),
+  kRpc(0x80);
+
+  @SuppressWarnings("MemberName")
+  private final int value;
+
+  NetworkTableType(int value) {
+    this.value = value;
+  }
+
+  public int getValue() {
+    return value;
+  }
+
+  /**
+   * Convert from the numerical representation of type to an enum type.
+   *
+   * @param value The numerical representation of kind
+   * @return The kind
+   */
+  public static NetworkTableType getFromInt(int value) {
+    switch (value) {
+      case 0x01: return kBoolean;
+      case 0x02: return kDouble;
+      case 0x04: return kString;
+      case 0x08: return kRaw;
+      case 0x10: return kBooleanArray;
+      case 0x20: return kDoubleArray;
+      case 0x40: return kStringArray;
+      case 0x80: return kRpc;
+      default: return kUnassigned;
+    }
+  }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableValue.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableValue.java
new file mode 100644
index 0000000..e1179ea
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableValue.java
@@ -0,0 +1,516 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+import java.util.Objects;
+
+/**
+ * A network table entry value.
+ */
+public final class NetworkTableValue {
+  NetworkTableValue(NetworkTableType type, Object value, long time) {
+    m_type = type;
+    m_value = value;
+    m_time = time;
+  }
+
+  NetworkTableValue(NetworkTableType type, Object value) {
+    this(type, value, NetworkTablesJNI.now());
+  }
+
+  NetworkTableValue(int type, Object value, long time) {
+    this(NetworkTableType.getFromInt(type), value, time);
+  }
+
+  /**
+   * Get the data type.
+   *
+   * @return The type.
+   */
+  public NetworkTableType getType() {
+    return m_type;
+  }
+
+  /**
+   * Get the data value stored.
+   *
+   * @return The type.
+   */
+  public Object getValue() {
+    return m_value;
+  }
+
+  /**
+   * Get the creation time of the value.
+   *
+   * @return The time, in the units returned by NetworkTablesJNI.now().
+   */
+  public long getTime() {
+    return m_time;
+  }
+
+  /*
+   * Type Checkers
+   */
+
+  /**
+   * Determine if entry value contains a value or is unassigned.
+   *
+   * @return True if the entry value contains a value.
+   */
+  public boolean isValid() {
+    return m_type != NetworkTableType.kUnassigned;
+  }
+
+  /**
+   * Determine if entry value contains a boolean.
+   *
+   * @return True if the entry value is of boolean type.
+   */
+  public boolean isBoolean() {
+    return m_type == NetworkTableType.kBoolean;
+  }
+
+  /**
+   * Determine if entry value contains a double.
+   *
+   * @return True if the entry value is of double type.
+   */
+  public boolean isDouble() {
+    return m_type == NetworkTableType.kDouble;
+  }
+
+  /**
+   * Determine if entry value contains a string.
+   *
+   * @return True if the entry value is of string type.
+   */
+  public boolean isString() {
+    return m_type == NetworkTableType.kString;
+  }
+
+  /**
+   * Determine if entry value contains a raw.
+   *
+   * @return True if the entry value is of raw type.
+   */
+  public boolean isRaw() {
+    return m_type == NetworkTableType.kRaw;
+  }
+
+  /**
+   * Determine if entry value contains a rpc definition.
+   *
+   * @return True if the entry value is of rpc definition type.
+   */
+  public boolean isRpc() {
+    return m_type == NetworkTableType.kRpc;
+  }
+
+  /**
+   * Determine if entry value contains a boolean array.
+   *
+   * @return True if the entry value is of boolean array type.
+   */
+  public boolean isBooleanArray() {
+    return m_type == NetworkTableType.kBooleanArray;
+  }
+
+  /**
+   * Determine if entry value contains a double array.
+   *
+   * @return True if the entry value is of double array type.
+   */
+  public boolean isDoubleArray() {
+    return m_type == NetworkTableType.kDoubleArray;
+  }
+
+  /**
+   * Determine if entry value contains a string array.
+   *
+   * @return True if the entry value is of string array type.
+   */
+  public boolean isStringArray() {
+    return m_type == NetworkTableType.kStringArray;
+  }
+
+  /*
+   * Type-Safe Getters
+   */
+
+  /**
+   * Get the entry's boolean value.
+   *
+   * @return The boolean value.
+   * @throws ClassCastException if the entry value is not of boolean type.
+   */
+  public boolean getBoolean() {
+    if (m_type != NetworkTableType.kBoolean) {
+      throw new ClassCastException("cannot convert " + m_type + " to boolean");
+    }
+    return ((Boolean) m_value).booleanValue();
+  }
+
+  /**
+   * Get the entry's double value.
+   *
+   * @return The double value.
+   * @throws ClassCastException if the entry value is not of double type.
+   */
+  public double getDouble() {
+    if (m_type != NetworkTableType.kDouble) {
+      throw new ClassCastException("cannot convert " + m_type + " to double");
+    }
+    return ((Number) m_value).doubleValue();
+  }
+
+  /**
+   * Get the entry's string value.
+   *
+   * @return The string value.
+   * @throws ClassCastException if the entry value is not of string type.
+   */
+  public String getString() {
+    if (m_type != NetworkTableType.kString) {
+      throw new ClassCastException("cannot convert " + m_type + " to string");
+    }
+    return (String) m_value;
+  }
+
+  /**
+   * Get the entry's raw value.
+   *
+   * @return The raw value.
+   * @throws ClassCastException if the entry value is not of raw type.
+   */
+  public byte[] getRaw() {
+    if (m_type != NetworkTableType.kRaw) {
+      throw new ClassCastException("cannot convert " + m_type + " to raw");
+    }
+    return (byte[]) m_value;
+  }
+
+  /**
+   * Get the entry's rpc definition value.
+   *
+   * @return The rpc definition value.
+   * @throws ClassCastException if the entry value is not of rpc definition type.
+   */
+  public byte[] getRpc() {
+    if (m_type != NetworkTableType.kRpc) {
+      throw new ClassCastException("cannot convert " + m_type + " to rpc");
+    }
+    return (byte[]) m_value;
+  }
+
+  /**
+   * Get the entry's boolean array value.
+   *
+   * @return The boolean array value.
+   * @throws ClassCastException if the entry value is not of boolean array type.
+   */
+  public boolean[] getBooleanArray() {
+    if (m_type != NetworkTableType.kBooleanArray) {
+      throw new ClassCastException("cannot convert " + m_type + " to boolean array");
+    }
+    return (boolean[]) m_value;
+  }
+
+  /**
+   * Get the entry's double array value.
+   *
+   * @return The double array value.
+   * @throws ClassCastException if the entry value is not of double array type.
+   */
+  public double[] getDoubleArray() {
+    if (m_type != NetworkTableType.kDoubleArray) {
+      throw new ClassCastException("cannot convert " + m_type + " to double array");
+    }
+    return (double[]) m_value;
+  }
+
+  /**
+   * Get the entry's string array value.
+   *
+   * @return The string array value.
+   * @throws ClassCastException if the entry value is not of string array type.
+   */
+  public String[] getStringArray() {
+    if (m_type != NetworkTableType.kStringArray) {
+      throw new ClassCastException("cannot convert " + m_type + " to string array");
+    }
+    return (String[]) m_value;
+  }
+
+  /*
+   * Factory functions.
+   */
+
+  /**
+   * Creates a boolean entry value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeBoolean(boolean value) {
+    return new NetworkTableValue(NetworkTableType.kBoolean, Boolean.valueOf(value));
+  }
+
+  /**
+   * Creates a boolean entry value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeBoolean(boolean value, long time) {
+    return new NetworkTableValue(NetworkTableType.kBoolean, Boolean.valueOf(value), time);
+  }
+
+  /**
+   * Creates a double entry value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeDouble(double value) {
+    return new NetworkTableValue(NetworkTableType.kDouble, Double.valueOf(value));
+  }
+
+  /**
+   * Creates a double entry value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeDouble(double value, long time) {
+    return new NetworkTableValue(NetworkTableType.kDouble, Double.valueOf(value), time);
+  }
+
+  /**
+   * Creates a string entry value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeString(String value) {
+    return new NetworkTableValue(NetworkTableType.kString, value);
+  }
+
+  /**
+   * Creates a string entry value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeString(String value, long time) {
+    return new NetworkTableValue(NetworkTableType.kString, value, time);
+  }
+
+  /**
+   * Creates a raw entry value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeRaw(byte[] value) {
+    return new NetworkTableValue(NetworkTableType.kRaw, value);
+  }
+
+  /**
+   * Creates a raw entry value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeRaw(byte[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kRaw, value, time);
+  }
+
+  /**
+   * Creates a rpc entry value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeRpc(byte[] value) {
+    return new NetworkTableValue(NetworkTableType.kRpc, value);
+  }
+
+  /**
+   * Creates a rpc entry value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeRpc(byte[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kRpc, value, time);
+  }
+
+  /**
+   * Creates a boolean array entry value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeBooleanArray(boolean[] value) {
+    return new NetworkTableValue(NetworkTableType.kBooleanArray, value);
+  }
+
+  /**
+   * Creates a boolean array entry value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeBooleanArray(boolean[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kBooleanArray, value, time);
+  }
+
+  /**
+   * Creates a boolean array entry value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeBooleanArray(Boolean[] value) {
+    return new NetworkTableValue(NetworkTableType.kBooleanArray, toNative(value));
+  }
+
+  /**
+   * Creates a boolean array entry value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeBooleanArray(Boolean[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kBooleanArray, toNative(value), time);
+  }
+
+  /**
+   * Creates a double array entry value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeDoubleArray(double[] value) {
+    return new NetworkTableValue(NetworkTableType.kDoubleArray, value);
+  }
+
+  /**
+   * Creates a double array entry value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeDoubleArray(double[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kDoubleArray, value, time);
+  }
+
+  /**
+   * Creates a double array entry value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeDoubleArray(Number[] value) {
+    return new NetworkTableValue(NetworkTableType.kDoubleArray, toNative(value));
+  }
+
+  /**
+   * Creates a double array entry value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeDoubleArray(Number[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kDoubleArray, toNative(value), time);
+  }
+
+  /**
+   * Creates a string array entry value.
+   *
+   * @param value the value
+   * @return The entry value
+   */
+  public static NetworkTableValue makeStringArray(String[] value) {
+    return new NetworkTableValue(NetworkTableType.kStringArray, value);
+  }
+
+  /**
+   * Creates a string array entry value.
+   *
+   * @param value the value
+   * @param time the creation time to use (instead of the current time)
+   * @return The entry value
+   */
+  public static NetworkTableValue makeStringArray(String[] value, long time) {
+    return new NetworkTableValue(NetworkTableType.kStringArray, value, time);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other == this) {
+      return true;
+    }
+    if (!(other instanceof NetworkTableValue)) {
+      return false;
+    }
+    NetworkTableValue ntOther = (NetworkTableValue) other;
+    return m_type == ntOther.m_type && m_value.equals(ntOther.m_value);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(m_type, m_value);
+  }
+
+  static boolean[] toNative(Boolean[] arr) {
+    boolean[] out = new boolean[arr.length];
+    for (int i = 0; i < arr.length; i++) {
+      out[i] = arr[i];
+    }
+    return out;
+  }
+
+  static double[] toNative(Number[] arr) {
+    double[] out = new double[arr.length];
+    for (int i = 0; i < arr.length; i++) {
+      out[i] = arr[i].doubleValue();
+    }
+    return out;
+  }
+
+  static Boolean[] fromNative(boolean[] arr) {
+    Boolean[] out = new Boolean[arr.length];
+    for (int i = 0; i < arr.length; i++) {
+      out[i] = arr[i];
+    }
+    return out;
+  }
+
+  static Double[] fromNative(double[] arr) {
+    Double[] out = new Double[arr.length];
+    for (int i = 0; i < arr.length; i++) {
+      out[i] = arr[i];
+    }
+    return out;
+  }
+
+  private NetworkTableType m_type;
+  private Object m_value;
+  private long m_time;
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTablesJNI.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTablesJNI.java
new file mode 100644
index 0000000..1d736fc
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTablesJNI.java
@@ -0,0 +1,153 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import edu.wpi.first.wpiutil.RuntimeLoader;
+
+public final class NetworkTablesJNI {
+  static boolean libraryLoaded = false;
+  static RuntimeLoader<NetworkTablesJNI> loader = null;
+
+  static {
+    if (!libraryLoaded) {
+      try {
+        loader = new RuntimeLoader<>("ntcorejni", RuntimeLoader.getDefaultExtractionRoot(), NetworkTablesJNI.class);
+        loader.loadLibrary();
+      } catch (IOException ex) {
+        ex.printStackTrace();
+        System.exit(1);
+      }
+      libraryLoaded = true;
+    }
+  }
+
+  public static native int getDefaultInstance();
+  public static native int createInstance();
+  public static native void destroyInstance(int inst);
+  public static native int getInstanceFromHandle(int handle);
+
+  public static native int getEntry(int inst, String key);
+  public static native int[] getEntries(int inst, String prefix, int types);
+  public static native String getEntryName(int entry);
+  public static native long getEntryLastChange(int entry);
+
+  public static native int getType(int entry);
+
+  public static native boolean setBoolean(int entry, long time, boolean value, boolean force);
+  public static native boolean setDouble(int entry, long time, double value, boolean force);
+  public static native boolean setString(int entry, long time, String value, boolean force);
+  public static native boolean setRaw(int entry, long time, byte[] value, boolean force);
+  public static native boolean setRaw(int entry, long time, ByteBuffer value, int len, boolean force);
+  public static native boolean setBooleanArray(int entry, long time, boolean[] value, boolean force);
+  public static native boolean setDoubleArray(int entry, long time, double[] value, boolean force);
+  public static native boolean setStringArray(int entry, long time, String[] value, boolean force);
+
+  public static native NetworkTableValue getValue(int entry);
+
+  public static native boolean getBoolean(int entry, boolean defaultValue);
+  public static native double getDouble(int entry, double defaultValue);
+  public static native String getString(int entry, String defaultValue);
+  public static native byte[] getRaw(int entry, byte[] defaultValue);
+  public static native boolean[] getBooleanArray(int entry, boolean[] defaultValue);
+  public static native double[] getDoubleArray(int entry, double[] defaultValue);
+  public static native String[] getStringArray(int entry, String[] defaultValue);
+  public static native boolean setDefaultBoolean(int entry, long time, boolean defaultValue);
+
+  public static native boolean setDefaultDouble(int entry, long time, double defaultValue);
+  public static native boolean setDefaultString(int entry, long time, String defaultValue);
+  public static native boolean setDefaultRaw(int entry, long time, byte[] defaultValue);
+  public static native boolean setDefaultBooleanArray(int entry, long time, boolean[] defaultValue);
+  public static native boolean setDefaultDoubleArray(int entry, long time, double[] defaultValue);
+  public static native boolean setDefaultStringArray(int entry, long time, String[] defaultValue);
+
+  public static native void setEntryFlags(int entry, int flags);
+  public static native int getEntryFlags(int entry);
+
+  public static native void deleteEntry(int entry);
+
+  public static native void deleteAllEntries(int inst);
+
+  public static native EntryInfo getEntryInfoHandle(NetworkTableInstance inst, int entry);
+  public static native EntryInfo[] getEntryInfo(NetworkTableInstance instObject, int inst, String prefix, int types);
+
+  public static native int createEntryListenerPoller(int inst);
+  public static native void destroyEntryListenerPoller(int poller);
+  public static native int addPolledEntryListener(int poller, String prefix, int flags);
+  public static native int addPolledEntryListener(int poller, int entry, int flags);
+  public static native EntryNotification[] pollEntryListener(NetworkTableInstance inst, int poller) throws InterruptedException;
+  public static native EntryNotification[] pollEntryListenerTimeout(NetworkTableInstance inst, int poller, double timeout) throws InterruptedException;
+  public static native void cancelPollEntryListener(int poller);
+  public static native void removeEntryListener(int entryListener);
+  public static native boolean waitForEntryListenerQueue(int inst, double timeout);
+
+  public static native int createConnectionListenerPoller(int inst);
+  public static native void destroyConnectionListenerPoller(int poller);
+  public static native int addPolledConnectionListener(int poller, boolean immediateNotify);
+  public static native ConnectionNotification[] pollConnectionListener(NetworkTableInstance inst, int poller) throws InterruptedException;
+  public static native ConnectionNotification[] pollConnectionListenerTimeout(NetworkTableInstance inst, int poller, double timeout) throws InterruptedException;
+  public static native void cancelPollConnectionListener(int poller);
+  public static native void removeConnectionListener(int connListener);
+  public static native boolean waitForConnectionListenerQueue(int inst, double timeout);
+
+  public static native int createRpcCallPoller(int inst);
+  public static native void destroyRpcCallPoller(int poller);
+  public static native void createPolledRpc(int entry, byte[] def, int poller);
+  public static native RpcAnswer[] pollRpc(NetworkTableInstance inst, int poller) throws InterruptedException;
+  public static native RpcAnswer[] pollRpcTimeout(NetworkTableInstance inst, int poller, double timeout) throws InterruptedException;
+  public static native void cancelPollRpc(int poller);
+  public static native boolean waitForRpcCallQueue(int inst, double timeout);
+  public static native boolean postRpcResponse(int entry, int call, byte[] result);
+  public static native int callRpc(int entry, byte[] params);
+  public static native byte[] getRpcResult(int entry, int call);
+  public static native byte[] getRpcResult(int entry, int call, double timeout);
+  public static native void cancelRpcResult(int entry, int call);
+
+  public static native byte[] getRpc(int entry, byte[] defaultValue);
+
+  public static native void setNetworkIdentity(int inst, String name);
+  public static native int getNetworkMode(int inst);
+  public static native void startServer(int inst, String persistFilename, String listenAddress, int port);
+  public static native void stopServer(int inst);
+  public static native void startClient(int inst);
+  public static native void startClient(int inst, String serverName, int port);
+  public static native void startClient(int inst, String[] serverNames, int[] ports);
+  public static native void startClientTeam(int inst, int team, int port);
+  public static native void stopClient(int inst);
+  public static native void setServer(int inst, String serverName, int port);
+  public static native void setServer(int inst, String[] serverNames, int[] ports);
+  public static native void setServerTeam(int inst, int team, int port);
+  public static native void startDSClient(int inst, int port);
+  public static native void stopDSClient(int inst);
+  public static native void setUpdateRate(int inst, double interval);
+
+  public static native void flush(int inst);
+
+  public static native ConnectionInfo[] getConnections(int inst);
+
+  public static native boolean isConnected(int inst);
+
+  public static native void savePersistent(int inst, String filename) throws PersistentException;
+  public static native String[] loadPersistent(int inst, String filename) throws PersistentException;  // returns warnings
+
+  public static native void saveEntries(int inst, String filename, String prefix) throws PersistentException;
+  public static native String[] loadEntries(int inst, String filename, String prefix) throws PersistentException;  // returns warnings
+
+  public static native long now();
+
+  public static native int createLoggerPoller(int inst);
+  public static native void destroyLoggerPoller(int poller);
+  public static native int addPolledLogger(int poller, int minLevel, int maxLevel);
+  public static native LogMessage[] pollLogger(NetworkTableInstance inst, int poller) throws InterruptedException;
+  public static native LogMessage[] pollLoggerTimeout(NetworkTableInstance inst, int poller, double timeout) throws InterruptedException;
+  public static native void cancelPollLogger(int poller);
+  public static native void removeLogger(int logger);
+  public static native boolean waitForLoggerQueue(int inst, double timeout);
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/PersistentException.java b/ntcore/src/main/java/edu/wpi/first/networktables/PersistentException.java
new file mode 100644
index 0000000..205b015
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/PersistentException.java
@@ -0,0 +1,21 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+import java.io.IOException;
+
+/**
+ * An exception thrown when persistent load/save fails in a {@link NetworkTable}.
+ */
+public final class PersistentException extends IOException {
+  public static final long serialVersionUID = 0;
+
+  public PersistentException(String message) {
+    super(message);
+  }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/RpcAnswer.java b/ntcore/src/main/java/edu/wpi/first/networktables/RpcAnswer.java
new file mode 100644
index 0000000..91e1aa4
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/RpcAnswer.java
@@ -0,0 +1,105 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+/**
+ * NetworkTables Remote Procedure Call (Server Side).
+ */
+public final class RpcAnswer {
+  /** Entry handle. */
+  @SuppressWarnings("MemberName")
+  public final int entry;
+
+  /** Call handle. */
+  @SuppressWarnings("MemberName")
+  public int call;
+
+  /** Entry name. */
+  @SuppressWarnings("MemberName")
+  public final String name;
+
+  /** Call raw parameters. */
+  @SuppressWarnings("MemberName")
+  public final byte[] params;
+
+  /** Connection that called the RPC. */
+  @SuppressWarnings("MemberName")
+  public final ConnectionInfo conn;
+
+  /** Constructor.
+   * This should generally only be used internally to NetworkTables.
+   *
+   * @param inst Instance
+   * @param entry Entry handle
+   * @param call Call handle
+   * @param name Entry name
+   * @param params Call raw parameters
+   * @param conn Connection info
+   */
+  public RpcAnswer(NetworkTableInstance inst, int entry, int call, String name, byte[] params,
+                   ConnectionInfo conn) {
+    this.m_inst = inst;
+    this.entry = entry;
+    this.call = call;
+    this.name = name;
+    this.params = params;
+    this.conn = conn;
+  }
+
+  static final byte[] emptyResponse = new byte[] {};
+
+  /*
+   * Finishes an RPC answer by replying empty if the user did not respond.
+   * Called internally by the callback thread.
+   */
+  void finish() {
+    if (call != 0) {
+      NetworkTablesJNI.postRpcResponse(entry, call, emptyResponse);
+      call = 0;
+    }
+  }
+
+  /**
+   * Determines if the native handle is valid.
+   *
+   * @return True if the native handle is valid, false otherwise.
+   */
+  public boolean isValid() {
+    return call != 0;
+  }
+
+  /**
+   * Post RPC response (return value) for a polled RPC.
+   *
+   * @param result  result raw data that will be provided to remote caller
+   * @return        true if the response was posted, otherwise false
+   */
+  public boolean postResponse(byte[] result) {
+    boolean ret = NetworkTablesJNI.postRpcResponse(entry, call, result);
+    call = 0;
+    return ret;
+  }
+
+  /* Network table instance. */
+  private final NetworkTableInstance m_inst;
+
+  /* Cached entry object. */
+  NetworkTableEntry m_entryObject;
+
+  /**
+   * Get the entry as an object.
+   *
+   * @return NetworkTableEntry for the RPC.
+   */
+  NetworkTableEntry getEntry() {
+    if (m_entryObject == null) {
+      m_entryObject = new NetworkTableEntry(m_inst, entry);
+    }
+    return m_entryObject;
+  }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/RpcCall.java b/ntcore/src/main/java/edu/wpi/first/networktables/RpcCall.java
new file mode 100644
index 0000000..1b377f6
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/RpcCall.java
@@ -0,0 +1,105 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+/**
+ * NetworkTables Remote Procedure Call.
+ */
+public final class RpcCall implements AutoCloseable {
+  /** Constructor.
+   * This should generally only be used internally to NetworkTables.
+   *
+   * @param entry Entry
+   * @param call Call handle
+   */
+  public RpcCall(NetworkTableEntry entry, int call) {
+    m_entry = entry;
+    m_call = call;
+  }
+
+  @Deprecated
+  public void free() {
+    close();
+  }
+
+  /**
+   * Cancels the result if no other action taken.
+   */
+  @Override
+  public synchronized void close() {
+    if (m_call != 0) {
+      cancelResult();
+    }
+  }
+
+  /**
+   * Determines if the native handle is valid.
+   *
+   * @return True if the native handle is valid, false otherwise.
+   */
+  public boolean isValid() {
+    return m_call != 0;
+  }
+
+  /**
+   * Get the RPC entry.
+   *
+   * @return NetworkTableEntry for the RPC.
+   */
+  public NetworkTableEntry getEntry() {
+    return m_entry;
+  }
+
+  /**
+   * Get the call native handle.
+   *
+   * @return Native handle.
+   */
+  public int getCall() {
+    return m_call;
+  }
+
+  /**
+   * Get the result (return value).  This function blocks until
+   * the result is received.
+   *
+   * @return Received result (output)
+   */
+  public byte[] getResult() {
+    byte[] result = NetworkTablesJNI.getRpcResult(m_entry.getHandle(), m_call);
+    if (result.length != 0) {
+      m_call = 0;
+    }
+    return result;
+  }
+
+  /**
+   * Get the result (return value).  This function blocks until
+   * the result is received or it times out.
+   *
+   * @param timeout     timeout, in seconds
+   * @return Received result (output)
+   */
+  public byte[] getResult(double timeout) {
+    byte[] result = NetworkTablesJNI.getRpcResult(m_entry.getHandle(), m_call, timeout);
+    if (result.length != 0) {
+      m_call = 0;
+    }
+    return result;
+  }
+
+  /**
+   * Ignore the result.  This function is non-blocking.
+   */
+  public void cancelResult() {
+    NetworkTablesJNI.cancelRpcResult(m_entry.getHandle(), m_call);
+  }
+
+  private final NetworkTableEntry m_entry;
+  private int m_call;
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/TableEntryListener.java b/ntcore/src/main/java/edu/wpi/first/networktables/TableEntryListener.java
new file mode 100644
index 0000000..676e57e
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/TableEntryListener.java
@@ -0,0 +1,27 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+/**
+ * A listener that listens to changes in values in a {@link NetworkTable}.
+ */
+@FunctionalInterface
+public interface TableEntryListener extends EntryListenerFlags {
+  /**
+   * Called when a key-value pair is changed in a {@link NetworkTable}.
+   *
+   * @param table the table the key-value pair exists in
+   * @param key the key associated with the value that changed
+   * @param entry the entry associated with the value that changed
+   * @param value the new value
+   * @param flags update flags; for example, EntryListenerFlags.kNew if the key
+   *     did not previously exist in the table
+   */
+  void valueChanged(NetworkTable table, String key, NetworkTableEntry entry,
+                    NetworkTableValue value, int flags);
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/TableListener.java b/ntcore/src/main/java/edu/wpi/first/networktables/TableListener.java
new file mode 100644
index 0000000..3c686e5
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/TableListener.java
@@ -0,0 +1,23 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+/**
+ * A listener that listens to new tables in a {@link NetworkTable}.
+ */
+@FunctionalInterface
+public interface TableListener {
+  /**
+   * Called when a new table is created within a {@link NetworkTable}.
+   *
+   * @param parent the parent of the table
+   * @param name the name of the new table
+   * @param table the new table
+   */
+  void tableCreated(NetworkTable parent, String name, NetworkTable table);
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/wpilibj/networktables/NetworkTable.java b/ntcore/src/main/java/edu/wpi/first/wpilibj/networktables/NetworkTable.java
new file mode 100644
index 0000000..f360a19
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/wpilibj/networktables/NetworkTable.java
@@ -0,0 +1,1189 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.wpilibj.networktables;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Consumer;
+
+import edu.wpi.first.networktables.ConnectionInfo;
+import edu.wpi.first.networktables.ConnectionNotification;
+import edu.wpi.first.networktables.EntryInfo;
+import edu.wpi.first.networktables.EntryNotification;
+import edu.wpi.first.networktables.NetworkTableEntry;
+import edu.wpi.first.networktables.NetworkTableInstance;
+import edu.wpi.first.networktables.NetworkTableType;
+import edu.wpi.first.networktables.NetworkTableValue;
+import edu.wpi.first.networktables.NetworkTablesJNI;
+import edu.wpi.first.networktables.PersistentException;
+import edu.wpi.first.wpilibj.tables.IRemote;
+import edu.wpi.first.wpilibj.tables.IRemoteConnectionListener;
+import edu.wpi.first.wpilibj.tables.ITable;
+import edu.wpi.first.wpilibj.tables.ITableListener;
+
+/**
+ * A network table that knows its subtable path.
+ * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable} instead.
+ */
+@Deprecated
+@SuppressWarnings("checkstyle:all")
+public class NetworkTable implements ITable, IRemote {
+  /**
+   * The path separator for sub-tables and keys
+   *
+   */
+  public static final char PATH_SEPARATOR = '/';
+  /**
+   * The default port that network tables operates on
+   */
+  public static final int DEFAULT_PORT = 1735;
+
+  private static boolean client = false;
+  private static boolean enableDS = true;
+  private static boolean running = false;
+  private static int port = DEFAULT_PORT;
+  private static String persistentFilename = "networktables.ini";
+
+  private synchronized static void checkInit() {
+    if (running)
+      throw new IllegalStateException(
+          "Network tables has already been initialized");
+  }
+
+  /**
+   * initializes network tables
+   * @deprecated Use {@link NetworkTableInstance#startServer()} or
+   * {@link NetworkTableInstance#startClient()} instead.
+   */
+  @Deprecated
+  public synchronized static void initialize() {
+    if (running)
+      shutdown();
+    NetworkTableInstance inst = NetworkTableInstance.getDefault();
+    if (client) {
+      inst.startClient();
+      if (enableDS)
+        inst.startDSClient(port);
+    } else
+      inst.startServer(persistentFilename, "", port);
+    running = true;
+  }
+
+  /**
+   * shuts down network tables
+   * @deprecated Use {@link NetworkTableInstance#stopServer()} or
+   * {@link NetworkTableInstance#stopClient()} instead.
+   */
+  @Deprecated
+  public synchronized static void shutdown() {
+    if (!running)
+      return;
+    NetworkTableInstance inst = NetworkTableInstance.getDefault();
+    if (client) {
+      inst.stopDSClient();
+      inst.stopClient();
+    } else
+      inst.stopServer();
+    running = false;
+  }
+
+  /**
+   * set that network tables should be a server
+   * This must be called before initialize or getTable
+   * @deprecated Use {@link NetworkTableInstance#startServer()} instead.
+   */
+  @Deprecated
+  public synchronized static void setServerMode() {
+    if (!client)
+      return;
+    checkInit();
+    client = false;
+  }
+
+  /**
+   * set that network tables should be a client
+   * This must be called before initialize or getTable
+   * @deprecated Use {@link NetworkTableInstance#startClient()} instead.
+   */
+  @Deprecated
+  public synchronized static void setClientMode() {
+    if (client)
+      return;
+    checkInit();
+    client = true;
+  }
+
+  /**
+   * set the team the robot is configured for (this will set the mdns address that
+   * network tables will connect to in client mode)
+   * This must be called before initialize or getTable
+   * @param team the team number
+   * @deprecated Use {@link NetworkTableInstance#setServerTeam(int)} or
+   * {@link NetworkTableInstance#startClientTeam(int)} instead.
+   */
+  @Deprecated
+  public synchronized static void setTeam(int team) {
+    NetworkTableInstance inst = NetworkTableInstance.getDefault();
+    inst.setServerTeam(team, port);
+    if (enableDS)
+      inst.startDSClient(port);
+  }
+
+  /**
+   * @param address the address that network tables will connect to in client
+   * mode
+   * @deprecated Use {@link NetworkTableInstance#setServer(String)} or
+   * {@link NetworkTableInstance#startClient(String)} instead.
+   */
+  @Deprecated
+  public synchronized static void setIPAddress(final String address) {
+    String[] addresses = new String[1];
+    addresses[0] = address;
+    setIPAddress(addresses);
+  }
+
+  /**
+   * @param addresses the adresses that network tables will connect to in
+   * client mode (in round robin order)
+   * @deprecated Use {@link NetworkTableInstance#setServer(String[])} or
+   * {@link NetworkTableInstance#startClient(String[])} instead.
+   */
+  @Deprecated
+  public synchronized static void setIPAddress(final String[] addresses) {
+    NetworkTableInstance inst = NetworkTableInstance.getDefault();
+    inst.setServer(addresses, port);
+
+    // Stop the DS client if we're explicitly connecting to localhost
+    if (addresses.length > 0 &&
+        (addresses[0].equals("localhost") || addresses[0].equals("127.0.0.1")))
+      inst.stopDSClient();
+    else if (enableDS)
+      inst.startDSClient(port);
+  }
+
+  /**
+   * Set the port number that network tables will connect to in client
+   * mode or listen to in server mode.
+   * @param aport the port number
+   * @deprecated Use the appropriate parameters to
+   * {@link NetworkTableInstance#setServer(String, int)},
+   * {@link NetworkTableInstance#startClient(String, int)},
+   * {@link NetworkTableInstance#startServer(String, String, int)}, and
+   * {@link NetworkTableInstance#startDSClient(int)} instead.
+   */
+  @Deprecated
+  public synchronized static void setPort(int aport) {
+    if (port == aport)
+      return;
+    checkInit();
+    port = aport;
+  }
+
+  /**
+   * Enable requesting the server address from the Driver Station.
+   * @param enabled whether to enable the connection to the local DS
+   * @deprecated Use {@link NetworkTableInstance#startDSClient()} and
+   * {@link NetworkTableInstance#stopDSClient()} instead.
+   */
+  @Deprecated
+  public synchronized static void setDSClientEnabled(boolean enabled) {
+    NetworkTableInstance inst = NetworkTableInstance.getDefault();
+    enableDS = enabled;
+    if (enableDS)
+      inst.startDSClient(port);
+    else
+      inst.stopDSClient();
+  }
+
+  /**
+   * Sets the persistent filename.
+   * @param filename the filename that the network tables server uses for
+   * automatic loading and saving of persistent values
+   * @deprecated Use the appropriate parameter to
+   * {@link NetworkTableInstance#startServer()} instead.
+   */
+  @Deprecated
+  public synchronized static void setPersistentFilename(final String filename) {
+    if (persistentFilename.equals(filename))
+      return;
+    checkInit();
+    persistentFilename = filename;
+  }
+
+  /**
+   * Sets the network identity.
+   * This is provided in the connection info on the remote end.
+   * @param name identity
+   * @deprecated Use {@link NetworkTableInstance#setNetworkIdentity(String)}
+   * instead.
+   */
+  @Deprecated
+  public static void setNetworkIdentity(String name) {
+    NetworkTableInstance.getDefault().setNetworkIdentity(name);
+  }
+
+  public static boolean[] toNative(Boolean[] arr) {
+    boolean[] out = new boolean[arr.length];
+    for (int i = 0; i < arr.length; i++)
+      out[i] = arr[i];
+    return out;
+  }
+
+  public static double[] toNative(Number[] arr) {
+    double[] out = new double[arr.length];
+    for (int i = 0; i < arr.length; i++)
+      out[i] = arr[i].doubleValue();
+    return out;
+  }
+
+  public static Boolean[] fromNative(boolean[] arr) {
+    Boolean[] out = new Boolean[arr.length];
+    for (int i = 0; i < arr.length; i++)
+      out[i] = arr[i];
+    return out;
+  }
+
+  public static Double[] fromNative(double[] arr) {
+    Double[] out = new Double[arr.length];
+    for (int i = 0; i < arr.length; i++)
+      out[i] = arr[i];
+    return out;
+  }
+
+  /**
+   * Gets the table with the specified key. If the table does not exist, a new
+   * table will be created.<br>
+   * This will automatically initialize network tables if it has not been
+   * already
+   *
+   * @deprecated Use {@link NetworkTableInstance#getTable(String)} instead.
+   *
+   * @param key   the key name
+   * @return the network table requested
+   */
+  @Deprecated
+  public synchronized static NetworkTable getTable(String key) {
+    if (!running)
+      initialize();
+    String theKey;
+    if (key.isEmpty() || key.equals("/")) {
+      theKey = "";
+    } else if (key.charAt(0) == NetworkTable.PATH_SEPARATOR) {
+      theKey = key;
+    } else {
+      theKey = NetworkTable.PATH_SEPARATOR + key;
+    }
+    return new NetworkTable(NetworkTableInstance.getDefault(), theKey);
+  }
+
+  private final String path;
+  private final String pathWithSep;
+  private final NetworkTableInstance inst;
+
+  NetworkTable(NetworkTableInstance inst, String path) {
+    this.path = path;
+    this.pathWithSep = path + PATH_SEPARATOR;
+    this.inst = inst;
+  }
+
+  @Override
+  public String toString() { return "NetworkTable: " + path; }
+
+  private final ConcurrentMap<String, NetworkTableEntry> entries = new ConcurrentHashMap<String, NetworkTableEntry>();
+
+  /**
+   * Gets the entry for a subkey.
+   * @param key the key name
+   * @return Network table entry.
+   */
+  private NetworkTableEntry getEntry(String key) {
+    NetworkTableEntry entry = entries.get(key);
+    if (entry == null) {
+      entry = inst.getEntry(pathWithSep + key);
+      entries.putIfAbsent(key, entry);
+    }
+    return entry;
+  }
+
+  /**
+   * Gets the current network connections.
+   * @return An array of connection information.
+   * @deprecated Use {@link NetworkTableInstance#getConnections()} instead.
+   */
+  @Deprecated
+  public static ConnectionInfo[] connections() {
+    return NetworkTableInstance.getDefault().getConnections();
+  }
+
+  /**
+   * Determine whether or not a network connection is active.
+   * @return True if connected, false if not connected.
+   * @deprecated Use {@link NetworkTableInstance#isConnected()} instead.
+   */
+  @Override
+  @Deprecated
+  public boolean isConnected() {
+    return inst.isConnected();
+  }
+
+  /**
+   * Determine whether NetworkTables is operating as a server or as a client.
+   * @return True if operating as a server, false otherwise.
+   * @deprecated Use {@link NetworkTableInstance#getNetworkMode()} instead.
+   */
+  @Override
+  @Deprecated
+  public boolean isServer() {
+    return (inst.getNetworkMode() & NetworkTableInstance.kNetModeServer) != 0;
+  }
+
+  /* Backwards compatibility shims for IRemoteConnectionListener */
+  private static class ConnectionListenerAdapter implements Consumer<ConnectionNotification> {
+    public int uid;
+    private final IRemote targetSource;
+    private final IRemoteConnectionListener targetListener;
+
+    public ConnectionListenerAdapter(IRemote targetSource, IRemoteConnectionListener targetListener) {
+      this.targetSource = targetSource;
+      this.targetListener = targetListener;
+    }
+
+    @Override
+    public void accept(ConnectionNotification event) {
+      if (event.connected)
+        targetListener.connectedEx(targetSource, event.conn);
+      else
+        targetListener.disconnectedEx(targetSource, event.conn);
+    }
+  }
+
+  private static final HashMap<IRemoteConnectionListener,ConnectionListenerAdapter> globalConnectionListenerMap = new HashMap<IRemoteConnectionListener,ConnectionListenerAdapter>();
+
+  private static IRemote staticRemote = new IRemote() {
+    @Override
+    public void addConnectionListener(IRemoteConnectionListener listener, boolean immediateNotify) {
+      NetworkTable.addGlobalConnectionListener(listener, immediateNotify);
+    }
+    @Override
+    public void removeConnectionListener(IRemoteConnectionListener listener) {
+      NetworkTable.removeGlobalConnectionListener(listener);
+    }
+    @Override
+    public boolean isConnected() {
+      ConnectionInfo[] conns = NetworkTableInstance.getDefault().getConnections();
+      return conns.length > 0;
+    }
+    @Override
+    public boolean isServer() {
+      return (NetworkTableInstance.getDefault().getNetworkMode() & NetworkTableInstance.kNetModeServer) != 0;
+    }
+  };
+
+  private final HashMap<IRemoteConnectionListener,ConnectionListenerAdapter> connectionListenerMap = new HashMap<IRemoteConnectionListener,ConnectionListenerAdapter>();
+
+  /**
+   * Add a connection listener.
+   * @param listener connection listener
+   * @param immediateNotify call listener immediately for all existing connections
+   * @deprecated Use {@link NetworkTableInstance#addConnectionListener(Consumer, boolean)} instead.
+   */
+  @Deprecated
+  public static synchronized void addGlobalConnectionListener(IRemoteConnectionListener listener, boolean immediateNotify) {
+    ConnectionListenerAdapter adapter = new ConnectionListenerAdapter(staticRemote, listener);
+    if (globalConnectionListenerMap.putIfAbsent(listener, adapter) != null) {
+      throw new IllegalStateException("Cannot add the same listener twice");
+    }
+    adapter.uid = NetworkTableInstance.getDefault().addConnectionListener(adapter, immediateNotify);
+  }
+
+  /**
+   * Remove a connection listener.
+   * @param listener connection listener
+   * @deprecated Use {@link NetworkTableInstance#removeConnectionListener(int)} instead.
+   */
+  @Deprecated
+  public static synchronized void removeGlobalConnectionListener(IRemoteConnectionListener listener) {
+    ConnectionListenerAdapter adapter = globalConnectionListenerMap.remove(listener);
+    if (adapter != null) {
+      NetworkTableInstance.getDefault().removeConnectionListener(adapter.uid);
+    }
+  }
+
+  /**
+   * Add a connection listener.
+   * @param listener connection listener
+   * @param immediateNotify call listener immediately for all existing connections
+   * @deprecated Use {@link NetworkTableInstance#addConnectionListener(Consumer, boolean)} instead.
+   */
+  @Override
+  @Deprecated
+  public synchronized void addConnectionListener(IRemoteConnectionListener listener,
+                                                 boolean immediateNotify) {
+    ConnectionListenerAdapter adapter = new ConnectionListenerAdapter(this, listener);
+    if (connectionListenerMap.putIfAbsent(listener, adapter) != null) {
+      throw new IllegalStateException("Cannot add the same listener twice");
+    }
+    adapter.uid = inst.addConnectionListener(adapter, immediateNotify);
+  }
+
+  /**
+   * Remove a connection listener.
+   * @param listener connection listener
+   * @deprecated Use {@link NetworkTableInstance#removeConnectionListener(int)} instead.
+   */
+  @Override
+  @Deprecated
+  public synchronized void removeConnectionListener(IRemoteConnectionListener listener) {
+    ConnectionListenerAdapter adapter = connectionListenerMap.get(listener);
+    if (adapter != null && connectionListenerMap.remove(listener, adapter)) {
+      inst.removeConnectionListener(adapter.uid);
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable#addEntryListener(TableEntryListener, int)} instead
+   * (with flags value of NOTIFY_NEW | NOTIFY_UPDATE).
+   */
+  @Override
+  @Deprecated
+  public void addTableListener(ITableListener listener) {
+    addTableListenerEx(listener, NOTIFY_NEW | NOTIFY_UPDATE);
+  }
+
+  /**
+   * {@inheritDoc}
+   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable#addEntryListener(TableEntryListener, int)} instead
+   * (with flags value of NOTIFY_NEW | NOTIFY_UPDATE | NOTIFY_IMMEDIATE).
+   */
+  @Override
+  @Deprecated
+  public void addTableListener(ITableListener listener,
+                               boolean immediateNotify) {
+    int flags = NOTIFY_NEW | NOTIFY_UPDATE;
+    if (immediateNotify)
+      flags |= NOTIFY_IMMEDIATE;
+    addTableListenerEx(listener, flags);
+  }
+
+  /* Base class for listeners; stores uid to implement remove functions */
+  private static class ListenerBase {
+    public int uid;
+  }
+
+  private static class OldTableListenerAdapter extends ListenerBase implements Consumer<EntryNotification> {
+    private final int prefixLen;
+    private final ITable targetSource;
+    private final ITableListener targetListener;
+
+    public OldTableListenerAdapter(int prefixLen, ITable targetSource, ITableListener targetListener) {
+      this.prefixLen = prefixLen;
+      this.targetSource = targetSource;
+      this.targetListener = targetListener;
+    }
+
+    @Override
+    public void accept(EntryNotification event) {
+      String relativeKey = event.name.substring(prefixLen);
+      if (relativeKey.indexOf(PATH_SEPARATOR) != -1)
+        return;
+      targetListener.valueChangedEx(targetSource, relativeKey, event.value.getValue(), event.flags);
+    }
+  }
+
+  private final HashMap<ITableListener,List<ListenerBase>> oldListenerMap = new HashMap<ITableListener,List<ListenerBase>>();
+
+  /**
+   * {@inheritDoc}
+   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable#addEntryListener(TableEntryListener, int)} instead.
+   */
+  @Override
+  @Deprecated
+  public synchronized void addTableListenerEx(ITableListener listener,
+                                              int flags) {
+    List<ListenerBase> adapters = oldListenerMap.get(listener);
+    if (adapters == null) {
+      adapters = new ArrayList<ListenerBase>();
+      oldListenerMap.put(listener, adapters);
+    }
+    OldTableListenerAdapter adapter =
+        new OldTableListenerAdapter(path.length() + 1, this, listener);
+    adapter.uid = inst.addEntryListener(pathWithSep, adapter, flags);
+    adapters.add(adapter);
+  }
+
+  /**
+   * {@inheritDoc}
+   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable#addEntryListener(String, TableEntryListener, int)}
+   * or {@link NetworkTableEntry#addListener(Consumer, int)} instead.
+   */
+  @Override
+  @Deprecated
+  public void addTableListener(String key, ITableListener listener,
+                               boolean immediateNotify) {
+    int flags = NOTIFY_NEW | NOTIFY_UPDATE;
+    if (immediateNotify)
+      flags |= NOTIFY_IMMEDIATE;
+    addTableListenerEx(key, listener, flags);
+  }
+
+  private static class OldKeyListenerAdapter extends ListenerBase implements Consumer<EntryNotification> {
+    private final String relativeKey;
+    private final ITable targetSource;
+    private final ITableListener targetListener;
+
+    public OldKeyListenerAdapter(String relativeKey, ITable targetSource, ITableListener targetListener) {
+      this.relativeKey = relativeKey;
+      this.targetSource = targetSource;
+      this.targetListener = targetListener;
+    }
+
+    @Override
+    public void accept(EntryNotification event) {
+      targetListener.valueChangedEx(targetSource, relativeKey, event.value.getValue(), event.flags);
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable#addEntryListener(String, TableEntryListener, int)}
+   * or {@link NetworkTableEntry#addListener(Consumer, int)} instead.
+   */
+  @Override
+  @Deprecated
+  public synchronized void addTableListenerEx(String key,
+                                              ITableListener listener,
+                                              int flags) {
+    List<ListenerBase> adapters = oldListenerMap.get(listener);
+    if (adapters == null) {
+      adapters = new ArrayList<ListenerBase>();
+      oldListenerMap.put(listener, adapters);
+    }
+    OldKeyListenerAdapter adapter = new OldKeyListenerAdapter(key, this, listener);
+    adapter.uid = inst.addEntryListener(getEntry(key), adapter, flags);
+    adapters.add(adapter);
+  }
+
+  /**
+   * {@inheritDoc}
+   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable#addSubTableListener(TableListener, boolean)}
+   * instead.
+   */
+  @Override
+  @Deprecated
+  public void addSubTableListener(final ITableListener listener) {
+    addSubTableListener(listener, false);
+  }
+
+  private static class OldSubListenerAdapter extends ListenerBase implements Consumer<EntryNotification> {
+    private final int prefixLen;
+    private final ITable targetSource;
+    private final ITableListener targetListener;
+    private final Set<String> notifiedTables = new HashSet<String>();
+
+    public OldSubListenerAdapter(int prefixLen, ITable targetSource, ITableListener targetListener) {
+      this.prefixLen = prefixLen;
+      this.targetSource = targetSource;
+      this.targetListener = targetListener;
+    }
+
+    @Override
+    public void accept(EntryNotification event) {
+      String relativeKey = event.name.substring(prefixLen);
+      int endSubTable = relativeKey.indexOf(PATH_SEPARATOR);
+      if (endSubTable == -1)
+        return;
+      String subTableKey = relativeKey.substring(0, endSubTable);
+      if (notifiedTables.contains(subTableKey))
+        return;
+      notifiedTables.add(subTableKey);
+      targetListener.valueChangedEx(targetSource, subTableKey, targetSource.getSubTable(subTableKey), event.flags);
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable#addSubTableListener(TableListener, boolean)}
+   * instead.
+   */
+  @Override
+  @Deprecated
+  public synchronized void addSubTableListener(final ITableListener listener,
+                                               boolean localNotify) {
+    List<ListenerBase> adapters = oldListenerMap.get(listener);
+    if (adapters == null) {
+      adapters = new ArrayList<ListenerBase>();
+      oldListenerMap.put(listener, adapters);
+    }
+    OldSubListenerAdapter adapter =
+        new OldSubListenerAdapter(path.length() + 1, this, listener);
+    int flags = NOTIFY_NEW | NOTIFY_IMMEDIATE;
+    if (localNotify)
+      flags |= NOTIFY_LOCAL;
+    adapter.uid = inst.addEntryListener(pathWithSep, adapter, flags);
+    adapters.add(adapter);
+  }
+
+  /**
+   * {@inheritDoc}
+   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable#removeTableListener(int)} instead.
+   */
+  @Override
+  @Deprecated
+  public synchronized void removeTableListener(ITableListener listener) {
+    List<ListenerBase> adapters = oldListenerMap.remove(listener);
+    if (adapters != null) {
+      for (ListenerBase adapter : adapters)
+        inst.removeEntryListener(adapter.uid);
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public ITable getSubTable(String key) {
+    return new NetworkTable(inst, pathWithSep + key);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean containsKey(String key) {
+    return getEntry(key).exists();
+  }
+
+  @Override
+  public boolean containsSubTable(String key) {
+    int[] handles = NetworkTablesJNI.getEntries(inst.getHandle(), pathWithSep + key + PATH_SEPARATOR, 0);
+    return handles.length != 0;
+  }
+
+  /**
+   * @param types bitmask of types; 0 is treated as a "don't care".
+   * @return keys currently in the table
+   */
+  @Override
+  public Set<String> getKeys(int types) {
+    Set<String> keys = new HashSet<String>();
+    int prefixLen = path.length() + 1;
+    for (EntryInfo info : inst.getEntryInfo(pathWithSep, types)) {
+      String relativeKey = info.name.substring(prefixLen);
+      if (relativeKey.indexOf(PATH_SEPARATOR) != -1)
+        continue;
+      keys.add(relativeKey);
+      // populate entries as we go
+      if (entries.get(relativeKey) == null) {
+        entries.putIfAbsent(relativeKey, new NetworkTableEntry(inst, info.entry));
+      }
+    }
+    return keys;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Set<String> getKeys() {
+    return getKeys(0);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Set<String> getSubTables() {
+    Set<String> keys = new HashSet<String>();
+    int prefixLen = path.length() + 1;
+    for (EntryInfo info : inst.getEntryInfo(pathWithSep, 0)) {
+      String relativeKey = info.name.substring(prefixLen);
+      int endSubTable = relativeKey.indexOf(PATH_SEPARATOR);
+      if (endSubTable == -1)
+        continue;
+      keys.add(relativeKey.substring(0, endSubTable));
+    }
+    return keys;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean putNumber(String key, double value) {
+    return getEntry(key).setNumber(value);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean setDefaultNumber(String key, double defaultValue) {
+    return getEntry(key).setDefaultDouble(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public double getNumber(String key, double defaultValue) {
+    return getEntry(key).getDouble(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean putString(String key, String value) {
+    return getEntry(key).setString(value);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean setDefaultString(String key, String defaultValue) {
+    return getEntry(key).setDefaultString(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public String getString(String key, String defaultValue) {
+    return getEntry(key).getString(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean putBoolean(String key, boolean value) {
+    return getEntry(key).setBoolean(value);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean setDefaultBoolean(String key, boolean defaultValue) {
+    return getEntry(key).setDefaultBoolean(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean getBoolean(String key, boolean defaultValue) {
+    return getEntry(key).getBoolean(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean putBooleanArray(String key, boolean[] value) {
+    return getEntry(key).setBooleanArray(value);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean putBooleanArray(String key, Boolean[] value) {
+    return getEntry(key).setBooleanArray(value);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean setDefaultBooleanArray(String key, boolean[] defaultValue) {
+    return getEntry(key).setDefaultBooleanArray(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean setDefaultBooleanArray(String key, Boolean[] defaultValue) {
+    return getEntry(key).setDefaultBooleanArray(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean[] getBooleanArray(String key, boolean[] defaultValue) {
+    return getEntry(key).getBooleanArray(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Boolean[] getBooleanArray(String key, Boolean[] defaultValue) {
+    return getEntry(key).getBooleanArray(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean putNumberArray(String key, double[] value) {
+    return getEntry(key).setDoubleArray(value);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean putNumberArray(String key, Double[] value) {
+    return getEntry(key).setNumberArray(value);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean setDefaultNumberArray(String key, double[] defaultValue) {
+    return getEntry(key).setDefaultDoubleArray(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean setDefaultNumberArray(String key, Double[] defaultValue) {
+    return getEntry(key).setDefaultNumberArray(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public double[] getNumberArray(String key, double[] defaultValue) {
+    return getEntry(key).getDoubleArray(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Double[] getNumberArray(String key, Double[] defaultValue) {
+    return getEntry(key).getDoubleArray(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean putStringArray(String key, String[] value) {
+    return getEntry(key).setStringArray(value);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean setDefaultStringArray(String key, String[] defaultValue) {
+    return getEntry(key).setDefaultStringArray(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public String[] getStringArray(String key, String[] defaultValue) {
+    return getEntry(key).getStringArray(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean putRaw(String key, byte[] value) {
+    return getEntry(key).setRaw(value);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean setDefaultRaw(String key, byte[] defaultValue) {
+    return getEntry(key).setDefaultRaw(defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean putRaw(String key, ByteBuffer value, int len) {
+    return getEntry(key).setRaw(value, len);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public byte[] getRaw(String key, byte[] defaultValue) {
+    return getEntry(key).getRaw(defaultValue);
+  }
+
+  /**
+   * Put a value in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  public boolean putValue(String key, NetworkTableValue value) {
+    return getEntry(key).setValue(value);
+  }
+
+  /**
+   * Sets the current value in the table if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doens't exist.
+   * @return False if the table key exists with a different type
+   */
+  public boolean setDefaultValue(String key, NetworkTableValue defaultValue) {
+    return getEntry(key).setDefaultValue(defaultValue);
+  }
+
+  /**
+   * Gets the value associated with a key as a NetworkTableValue object.
+   * @param key the key of the value to look up
+   * @return the value associated with the given key
+   */
+  public NetworkTableValue getValue(String key) {
+    return getEntry(key).getValue();
+  }
+
+  /**
+   * {@inheritDoc}
+   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTableEntry#setValue(Object)}
+   * instead, e.g. `NetworkTable.getEntry(key).setValue(NetworkTableEntry.makeBoolean(false));`
+   * or `NetworkTable.getEntry(key).setValue(new Boolean(false));`
+   */
+  @Override
+  @Deprecated
+  public boolean putValue(String key, Object value) throws IllegalArgumentException {
+    if (value instanceof Boolean)
+      return putBoolean(key, ((Boolean)value).booleanValue());
+    else if (value instanceof Number)
+      return putDouble(key, ((Number)value).doubleValue());
+    else if (value instanceof String)
+      return putString(key, (String)value);
+    else if (value instanceof byte[])
+      return putRaw(key, (byte[])value);
+    else if (value instanceof boolean[])
+      return putBooleanArray(key, (boolean[])value);
+    else if (value instanceof double[])
+      return putNumberArray(key, (double[])value);
+    else if (value instanceof Boolean[])
+      return putBooleanArray(key, toNative((Boolean[])value));
+    else if (value instanceof Number[])
+      return putNumberArray(key, toNative((Number[])value));
+    else if (value instanceof String[])
+      return putStringArray(key, (String[])value);
+    else if (value instanceof NetworkTableValue)
+      return getEntry(key).setValue((NetworkTableValue)value);
+    else
+      throw new IllegalArgumentException("Value of type " + value.getClass().getName() + " cannot be put into a table");
+  }
+
+  /**
+   * {@inheritDoc}
+   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTableEntry#getValue()}
+   * instead, e.g. `NetworkTable.getEntry(key).getValue();`
+   */
+  @Override
+  @Deprecated
+  public Object getValue(String key, Object defaultValue) {
+    NetworkTableValue value = getValue(key);
+    if (value.getType() == NetworkTableType.kUnassigned) {
+      return defaultValue;
+    }
+    return value.getValue();
+  }
+
+  /** The persistent flag value. */
+  public static final int PERSISTENT = 1;
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void setPersistent(String key) {
+    getEntry(key).setPersistent();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void clearPersistent(String key) {
+    getEntry(key).clearPersistent();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean isPersistent(String key) {
+    return getEntry(key).isPersistent();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void setFlags(String key, int flags) {
+    getEntry(key).setFlags(flags);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void clearFlags(String key, int flags) {
+    getEntry(key).clearFlags(flags);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public int getFlags(String key) {
+    return getEntry(key).getFlags();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void delete(String key) {
+    getEntry(key).delete();
+  }
+
+  /**
+   * Deletes ALL keys in ALL subtables.  Use with caution!
+   * @deprecated Use {@link NetworkTableInstance#deleteAllEntries()} instead.
+   */
+  @Deprecated
+  public static void globalDeleteAll() {
+    NetworkTableInstance.getDefault().deleteAllEntries();
+  }
+
+  /**
+   * Flushes all updated values immediately to the network.
+   * Note: This is rate-limited to protect the network from flooding.
+   * This is primarily useful for synchronizing network updates with
+   * user code.
+   * @deprecated Use {@link NetworkTableInstance#flush()} instead.
+   */
+  @Deprecated
+  public static void flush() {
+    NetworkTableInstance.getDefault().flush();
+  }
+
+  /**
+   * Set the periodic update rate.
+   *
+   * @param interval update interval in seconds (range 0.01 to 1.0)
+   * @deprecated Use {@link NetworkTableInstance#setUpdateRate(double)}
+   * instead.
+   */
+  @Deprecated
+  public static void setUpdateRate(double interval) {
+    NetworkTableInstance.getDefault().setUpdateRate(interval);
+  }
+
+  /**
+   * Saves persistent keys to a file.  The server does this automatically.
+   *
+   * @param filename file name
+   * @throws PersistentException if error saving file
+   * @deprecated Use {@link NetworkTableInstance#savePersistent(String)}
+   * instead.
+   */
+  @Deprecated
+  public static void savePersistent(String filename) throws PersistentException {
+    NetworkTableInstance.getDefault().savePersistent(filename);
+  }
+
+  /**
+   * Loads persistent keys from a file.  The server does this automatically.
+   *
+   * @param filename file name
+   * @return List of warnings (errors result in an exception instead)
+   * @throws PersistentException if error reading file
+   * @deprecated Use {@link NetworkTableInstance#loadPersistent(String)}
+   * instead.
+   */
+  @Deprecated
+  public static String[] loadPersistent(String filename) throws PersistentException {
+    return NetworkTableInstance.getDefault().loadPersistent(filename);
+  }
+
+  /*
+   * Deprecated Methods
+   */
+
+  /**
+   * {@inheritDoc}
+   * @deprecated Use {@link #putNumber(String, double)} instead.
+   */
+  @Override
+  @Deprecated
+  public boolean putDouble(String key, double value) {
+    return putNumber(key, value);
+  }
+
+  /**
+   * {@inheritDoc}
+   * @deprecated Use {@link #getNumber(String, double)} instead.
+   */
+  @Override
+  @Deprecated
+  public double getDouble(String key, double defaultValue) {
+    return getNumber(key, defaultValue);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public String getPath() {
+    return path;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) {
+      return true;
+    }
+    if (!(o instanceof NetworkTable)) {
+      return false;
+    }
+    NetworkTable other = (NetworkTable) o;
+    return inst.equals(other.inst) && path.equals(other.path);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(inst, path);
+  }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/wpilibj/tables/IRemote.java b/ntcore/src/main/java/edu/wpi/first/wpilibj/tables/IRemote.java
new file mode 100644
index 0000000..22b6f96
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/wpilibj/tables/IRemote.java
@@ -0,0 +1,44 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.wpilibj.tables;
+
+
+/**
+ * Represents an object that has a remote connection.
+ * @deprecated Use {@link edu.wpi.first.networktables.NetworkTableInstance}.
+ */
+@Deprecated
+@SuppressWarnings("checkstyle:all")
+public interface IRemote {
+  /**
+   * Register an object to listen for connection and disconnection events
+   *
+   * @param listener the listener to be register
+   * @param immediateNotify if the listener object should be notified of the current connection state
+   */
+  public void addConnectionListener(IRemoteConnectionListener listener, boolean immediateNotify);
+
+  /**
+   * Unregister a listener from connection events
+   *
+   * @param listener the listener to be unregistered
+   */
+  public void removeConnectionListener(IRemoteConnectionListener listener);
+
+  /**
+   * Get the current state of the objects connection
+   * @return the current connection state
+   */
+    public boolean isConnected();
+
+  /**
+   * If the object is acting as a server
+   * @return if the object is a server
+   */
+    public boolean isServer();
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/wpilibj/tables/IRemoteConnectionListener.java b/ntcore/src/main/java/edu/wpi/first/wpilibj/tables/IRemoteConnectionListener.java
new file mode 100644
index 0000000..a3ff118
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/wpilibj/tables/IRemoteConnectionListener.java
@@ -0,0 +1,47 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.wpilibj.tables;
+
+import edu.wpi.first.networktables.ConnectionInfo;
+
+/**
+ * A listener that listens for connection changes in a {@link IRemote} object.
+ * @deprecated Use Consumer&lt;{@link edu.wpi.first.networktables.ConnectionNotification}&gt;.
+ */
+@Deprecated
+@SuppressWarnings("checkstyle:all")
+public interface IRemoteConnectionListener {
+  /**
+   * Called when an IRemote is connected
+   * @param remote the object that connected
+   */
+  public void connected(IRemote remote);
+  /**
+   * Called when an IRemote is disconnected
+   * @param remote the object that disconnected
+   */
+  public void disconnected(IRemote remote);
+  /**
+   * Extended version of connected called when an IRemote is connected.
+    * Contains the connection info of the connected remote
+   * @param remote the object that connected
+   * @param info the connection info for the connected remote
+   */
+  default public void connectedEx(IRemote remote, ConnectionInfo info) {
+    connected(remote);
+  }
+  /**
+   * Extended version of connected called when an IRemote is disconnected.
+   * Contains the connection info of the disconnected remote
+   * @param remote the object that disconnected
+   * @param info the connection info for the disconnected remote
+   */
+  default public void disconnectedEx(IRemote remote, ConnectionInfo info) {
+    disconnected(remote);
+  }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/wpilibj/tables/ITable.java b/ntcore/src/main/java/edu/wpi/first/wpilibj/tables/ITable.java
new file mode 100644
index 0000000..2d0d5d6
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/wpilibj/tables/ITable.java
@@ -0,0 +1,488 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.wpilibj.tables;
+
+import java.nio.ByteBuffer;
+import java.util.Set;
+
+
+/**
+ * A table whose values can be read and written to.
+ * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable}.
+ */
+@Deprecated
+@SuppressWarnings("checkstyle:all")
+public interface ITable {
+
+  /**
+   * Checks the table and tells if it contains the specified key
+   *
+   * @param key the key to search for
+   * @return true if the table as a value assigned to the given key
+   */
+  public boolean containsKey(String key);
+
+  /**
+   * @param key the key to search for
+   * @return true if there is a subtable with the key which contains at least
+   * one key/subtable of its own
+   */
+  public boolean containsSubTable(String key);
+
+  /**
+   * Returns the table at the specified key. If there is no table at the
+   * specified key, it will create a new table
+   *
+   * @param key the name of the table relative to this one
+   * @return a sub table relative to this one
+   */
+  public ITable getSubTable(String key);
+
+  /**
+   * Gets all keys in the table (not including sub-tables).
+   * @param types bitmask of types; 0 is treated as a "don't care".
+   * @return keys currently in the table
+   */
+  public Set<String> getKeys(int types);
+
+  /**
+   * Gets all keys in the table (not including sub-tables).
+   * @return keys currently in the table
+   */
+  public Set<String> getKeys();
+
+  /**
+   * Gets the names of all subtables in the table.
+   * @return subtables currently in the table
+   */
+  public Set<String> getSubTables();
+
+  /**
+   * Makes a key's value persistent through program restarts.
+   * The key cannot be null.
+   *
+   * @param key the key name
+   */
+  public void setPersistent(String key);
+
+  /**
+   * Stop making a key's value persistent through program restarts.
+   * The key cannot be null.
+   *
+   * @param key the key name
+   */
+  public void clearPersistent(String key);
+
+  /**
+   * Returns whether the value is persistent through program restarts.
+   * The key cannot be null.
+   *
+   * @param key the key name
+   * @return True if the value is persistent.
+   */
+  public boolean isPersistent(String key);
+
+  /**
+   * Sets flags on the specified key in this table. The key can
+   * not be null.
+   *
+   * @param key the key name
+   * @param flags the flags to set (bitmask)
+   */
+  public void setFlags(String key, int flags);
+
+  /**
+   * Clears flags on the specified key in this table. The key can
+   * not be null.
+   *
+   * @param key the key name
+   * @param flags the flags to clear (bitmask)
+   */
+  public void clearFlags(String key, int flags);
+
+  /**
+   * Returns the flags for the specified key.
+   *
+   * @param key the key name
+   * @return the flags, or 0 if the key is not defined
+   */
+  public int getFlags(String key);
+
+  /**
+   * Deletes the specified key in this table. The key can
+   * not be null.
+   *
+   * @param key the key name
+   */
+  public void delete(String key);
+
+  /**
+   * Gets the value associated with a key as an object.
+   * NOTE: If the value is a double, it will return a Double object,
+   * not a primitive.  To get the primitive, use
+   * {@link #getDouble(String, double)}.
+   * @param key the key of the value to look up
+   * @param defaultValue the default value if the key is null
+   * @return the value associated with the given key
+   */
+  public Object getValue(String key, Object defaultValue);
+
+  /**
+   * Put a value in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   * @throws IllegalArgumentException when the value is not supported by the
+   * table
+   */
+  public boolean putValue(String key, Object value)
+      throws IllegalArgumentException;
+
+  /**
+   * Put a number in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  public boolean putNumber(String key, double value);
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doens't exist.
+   * @return False if the table key exists with a different type
+   */
+  public boolean setDefaultNumber(String key, double defaultValue);
+
+  /**
+   * Returns the number the key maps to. If the key does not exist or is of
+   * different type, it will return the default value.
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  public double getNumber(String key, double defaultValue);
+
+  /**
+   * Put a string in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  public boolean putString(String key, String value);
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doens't exist.
+   * @return False if the table key exists with a different type
+   */
+  public boolean setDefaultString(String key, String defaultValue);
+
+  /**
+   * Returns the string the key maps to. If the key does not exist or is of
+   * different type, it will return the default value.
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  public String getString(String key, String defaultValue);
+
+  /**
+   * Put a boolean in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  public boolean putBoolean(String key, boolean value);
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doens't exist.
+   * @return False if the table key exists with a different type
+   */
+  public boolean setDefaultBoolean(String key, boolean defaultValue);
+
+  /**
+   * Returns the boolean the key maps to. If the key does not exist or is of
+   * different type, it will return the default value.
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  public boolean getBoolean(String key, boolean defaultValue);
+
+  /**
+   * Put a boolean array in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  public boolean putBooleanArray(String key, boolean[] value);
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doens't exist.
+   * @return False if the table key exists with a different type
+   */
+  public boolean setDefaultBooleanArray(String key, boolean[] defaultValue);
+
+  /**
+   * Put a boolean array in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  public boolean putBooleanArray(String key, Boolean[] value);
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doens't exist.
+   * @return False if the table key exists with a different type
+   */
+  public boolean setDefaultBooleanArray(String key, Boolean[] defaultValue);
+
+  /**
+   * Returns the boolean array the key maps to. If the key does not exist or is
+   * of different type, it will return the default value.
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  public boolean[] getBooleanArray(String key, boolean[] defaultValue);
+  /**
+   * Returns the boolean array the key maps to. If the key does not exist or is
+   * of different type, it will return the default value.
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  public Boolean[] getBooleanArray(String key, Boolean[] defaultValue);
+
+  /**
+   * Put a number array in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  public boolean putNumberArray(String key, double[] value);
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doens't exist.
+   * @return False if the table key exists with a different type
+   */
+  public boolean setDefaultNumberArray(String key, double[] defaultValue);
+
+  /**
+   * Put a number array in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  public boolean putNumberArray(String key, Double[] value);
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doens't exist.
+   * @return False if the table key exists with a different type
+   */
+  public boolean setDefaultNumberArray(String key, Double[] defaultValue);
+
+  /**
+   * Returns the number array the key maps to. If the key does not exist or is
+   * of different type, it will return the default value.
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  public double[] getNumberArray(String key, double[] defaultValue);
+  /**
+   * Returns the number array the key maps to. If the key does not exist or is
+   * of different type, it will return the default value.
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  public Double[] getNumberArray(String key, Double[] defaultValue);
+
+  /**
+   * Put a string array in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  public boolean putStringArray(String key, String[] value);
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doens't exist.
+   * @return False if the table key exists with a different type
+   */
+  public boolean setDefaultStringArray(String key, String[] defaultValue);
+
+  /**
+   * Returns the string array the key maps to. If the key does not exist or is
+   * of different type, it will return the default value.
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  public String[] getStringArray(String key, String[] defaultValue);
+
+  /**
+   * Put a raw value (byte array) in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  public boolean putRaw(String key, byte[] value);
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doens't exist.
+   * @return False if the table key exists with a different type
+   */
+  public boolean setDefaultRaw(String key, byte[] defaultValue);
+
+  /**
+   * Put a raw value (bytes from a byte buffer) in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @param len the length of the value
+   * @return False if the table key already exists with a different type
+   */
+  public boolean putRaw(String key, ByteBuffer value, int len);
+
+  /**
+   * Returns the raw value (byte array) the key maps to. If the key does not
+   * exist or is of different type, it will return the default value.
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  public byte[] getRaw(String key, byte[] defaultValue);
+
+  /** Notifier flag values. */
+  public static final int NOTIFY_IMMEDIATE = 0x01;
+  public static final int NOTIFY_LOCAL = 0x02;
+  public static final int NOTIFY_NEW = 0x04;
+  public static final int NOTIFY_DELETE = 0x08;
+  public static final int NOTIFY_UPDATE = 0x10;
+  public static final int NOTIFY_FLAGS = 0x20;
+
+  /**
+   * Add a listener for changes to the table
+   * @param listener the listener to add
+   */
+  public void addTableListener(ITableListener listener);
+  /**
+   * Add a listener for changes to the table
+   * @param listener the listener to add
+   * @param immediateNotify if true then this listener will be notified of all
+   * current entries (marked as new)
+   */
+  public void addTableListener(ITableListener listener,
+                               boolean immediateNotify);
+  /**
+   * Add a listener for changes to the table
+   * @param listener the listener to add
+   * @param flags bitmask specifying desired notifications
+   */
+  public void addTableListenerEx(ITableListener listener, int flags);
+
+  /**
+   * Add a listener for changes to a specific key the table
+   * @param key the key to listen for
+   * @param listener the listener to add
+   * @param immediateNotify if true then this listener will be notified of all
+   * current entries (marked as new)
+   */
+  public void addTableListener(String key, ITableListener listener,
+                               boolean immediateNotify);
+  /**
+   * Add a listener for changes to a specific key the table
+   * @param key the key to listen for
+   * @param listener the listener to add
+   * @param flags bitmask specifying desired notifications
+   */
+  public void addTableListenerEx(String key, ITableListener listener,
+                                 int flags);
+  /**
+   * This will immediately notify the listener of all current sub tables
+   * @param listener the listener to notify
+   */
+  public void addSubTableListener(final ITableListener listener);
+  /**
+   * This will immediately notify the listener of all current sub tables
+   * @param listener the listener to notify
+   * @param localNotify if true then this listener will be notified of all
+   * local changes in addition to all remote changes
+   */
+  public void addSubTableListener(final ITableListener listener,
+                                  boolean localNotify);
+  /**
+   * Remove a listener from receiving table events
+   * @param listener the listener to be removed
+   */
+  public void removeTableListener(ITableListener listener);
+
+  /*
+   * Deprecated Methods
+   */
+
+  /**
+   * Maps the specified key to the specified value in this table.
+   * The key can not be null.
+   * The value can be retrieved by calling the get method with a key that is
+   * equal to the original key.
+   * @param key the key
+   * @param value the value
+   * @return False if the table key already exists with a different type
+   * @throws IllegalArgumentException if key is null
+   * @deprecated Use {@link #putNumber(String, double)} instead.
+   */
+  @Deprecated
+  public boolean putDouble(String key, double value);
+
+  /**
+   * Returns the value at the specified key.
+   * @param key the key
+   * @param defaultValue the value returned if the key is undefined
+   * @return the value
+   * @throws IllegalArgumentException if the value mapped to by the key is not a
+   * double
+   * @throws IllegalArgumentException if the key is null
+   * @deprecated Use {@link #getNumber(String, double)} instead.
+   */
+  @Deprecated
+  public double getDouble(String key, double defaultValue);
+
+  /**
+   * Gets the full path of this table.  Does not include the trailing "/".
+   * @return The path to this table (e.g. "", "/foo").
+   */
+  public String getPath();
+
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/wpilibj/tables/ITableListener.java b/ntcore/src/main/java/edu/wpi/first/wpilibj/tables/ITableListener.java
new file mode 100644
index 0000000..b08312b
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/wpilibj/tables/ITableListener.java
@@ -0,0 +1,44 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.wpilibj.tables;
+
+/**
+ * A listener that listens to changes in values in a {@link ITable}.
+ * @deprecated Use Consumer&lt;{@link edu.wpi.first.networktables.EntryNotification}&gt;,
+ * {@link edu.wpi.first.networktables.TableEntryListener}, or
+ * {@link edu.wpi.first.networktables.TableListener} as appropriate.
+ */
+@FunctionalInterface
+@Deprecated
+@SuppressWarnings("checkstyle:all")
+public interface ITableListener {
+    /**
+     * Called when a key-value pair is changed in a {@link ITable}
+     * @param source the table the key-value pair exists in
+     * @param key the key associated with the value that changed
+     * @param value the new value
+     * @param isNew true if the key did not previously exist in the table, otherwise it is false
+     */
+    public void valueChanged(ITable source, String key, Object value, boolean isNew);
+
+    /**
+     * Extended version of valueChanged.  Called when a key-value pair is
+     * changed in a {@link ITable}.  The default implementation simply calls
+     * valueChanged().  If this is overridden, valueChanged() will not be
+     * called.
+     * @param source the table the key-value pair exists in
+     * @param key the key associated with the value that changed
+     * @param value the new value
+     * @param flags update flags; for example, NOTIFY_NEW if the key did not
+     * previously exist in the table
+     */
+    default public void valueChangedEx(ITable source, String key, Object value, int flags) {
+        // NOTIFY_NEW = 0x04
+        valueChanged(source, key, value, (flags & 0x04) != 0);
+    }
+}
diff --git a/ntcore/src/main/native/cpp/CallbackManager.h b/ntcore/src/main/native/cpp/CallbackManager.h
new file mode 100644
index 0000000..0fd9617
--- /dev/null
+++ b/ntcore/src/main/native/cpp/CallbackManager.h
@@ -0,0 +1,327 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_CALLBACKMANAGER_H_
+#define NTCORE_CALLBACKMANAGER_H_
+
+#include <atomic>
+#include <climits>
+#include <functional>
+#include <memory>
+#include <queue>
+#include <utility>
+#include <vector>
+
+#include <wpi/SafeThread.h>
+#include <wpi/UidVector.h>
+#include <wpi/condition_variable.h>
+#include <wpi/mutex.h>
+#include <wpi/raw_ostream.h>
+
+namespace nt {
+
+namespace impl {
+
+template <typename Callback>
+class ListenerData {
+ public:
+  ListenerData() = default;
+  explicit ListenerData(Callback callback_) : callback(callback_) {}
+  explicit ListenerData(unsigned int poller_uid_) : poller_uid(poller_uid_) {}
+
+  explicit operator bool() const { return callback || poller_uid != UINT_MAX; }
+
+  Callback callback;
+  unsigned int poller_uid = UINT_MAX;
+};
+
+// CRTP callback manager thread
+// @tparam Derived        derived class
+// @tparam NotifierData   data buffered for each callback
+// @tparam ListenerData   data stored for each listener
+// Derived must define the following functions:
+//   bool Matches(const ListenerData& listener, const NotifierData& data);
+//   void SetListener(NotifierData* data, unsigned int listener_uid);
+//   void DoCallback(Callback callback, const NotifierData& data);
+template <typename Derived, typename TUserInfo,
+          typename TListenerData =
+              ListenerData<std::function<void(const TUserInfo& info)>>,
+          typename TNotifierData = TUserInfo>
+class CallbackThread : public wpi::SafeThread {
+ public:
+  typedef TUserInfo UserInfo;
+  typedef TNotifierData NotifierData;
+  typedef TListenerData ListenerData;
+
+  ~CallbackThread() {
+    // Wake up any blocked pollers
+    for (size_t i = 0; i < m_pollers.size(); ++i) {
+      if (auto poller = m_pollers[i]) poller->Terminate();
+    }
+  }
+
+  void Main() override;
+
+  wpi::UidVector<ListenerData, 64> m_listeners;
+
+  std::queue<std::pair<unsigned int, NotifierData>> m_queue;
+  wpi::condition_variable m_queue_empty;
+
+  struct Poller {
+    void Terminate() {
+      {
+        std::lock_guard<wpi::mutex> lock(poll_mutex);
+        terminating = true;
+      }
+      poll_cond.notify_all();
+    }
+    std::queue<NotifierData> poll_queue;
+    wpi::mutex poll_mutex;
+    wpi::condition_variable poll_cond;
+    bool terminating = false;
+    bool cancelling = false;
+  };
+  wpi::UidVector<std::shared_ptr<Poller>, 64> m_pollers;
+
+  // Must be called with m_mutex held
+  template <typename... Args>
+  void SendPoller(unsigned int poller_uid, Args&&... args) {
+    if (poller_uid > m_pollers.size()) return;
+    auto poller = m_pollers[poller_uid];
+    if (!poller) return;
+    {
+      std::lock_guard<wpi::mutex> lock(poller->poll_mutex);
+      poller->poll_queue.emplace(std::forward<Args>(args)...);
+    }
+    poller->poll_cond.notify_one();
+  }
+};
+
+template <typename Derived, typename TUserInfo, typename TListenerData,
+          typename TNotifierData>
+void CallbackThread<Derived, TUserInfo, TListenerData, TNotifierData>::Main() {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  while (m_active) {
+    while (m_queue.empty()) {
+      m_cond.wait(lock);
+      if (!m_active) return;
+    }
+
+    while (!m_queue.empty()) {
+      if (!m_active) return;
+      auto item = std::move(m_queue.front());
+
+      if (item.first != UINT_MAX) {
+        if (item.first < m_listeners.size()) {
+          auto& listener = m_listeners[item.first];
+          if (listener &&
+              static_cast<Derived*>(this)->Matches(listener, item.second)) {
+            static_cast<Derived*>(this)->SetListener(&item.second, item.first);
+            if (listener.callback) {
+              lock.unlock();
+              static_cast<Derived*>(this)->DoCallback(listener.callback,
+                                                      item.second);
+              lock.lock();
+            } else if (listener.poller_uid != UINT_MAX) {
+              SendPoller(listener.poller_uid, std::move(item.second));
+            }
+          }
+        }
+      } else {
+        // Use index because iterator might get invalidated.
+        for (size_t i = 0; i < m_listeners.size(); ++i) {
+          auto& listener = m_listeners[i];
+          if (!listener) continue;
+          if (!static_cast<Derived*>(this)->Matches(listener, item.second))
+            continue;
+          static_cast<Derived*>(this)->SetListener(&item.second, i);
+          if (listener.callback) {
+            lock.unlock();
+            static_cast<Derived*>(this)->DoCallback(listener.callback,
+                                                    item.second);
+            lock.lock();
+          } else if (listener.poller_uid != UINT_MAX) {
+            SendPoller(listener.poller_uid, item.second);
+          }
+        }
+      }
+      m_queue.pop();
+    }
+
+    m_queue_empty.notify_all();
+  }
+}
+
+}  // namespace impl
+
+// CRTP callback manager
+// @tparam Derived  derived class
+// @tparam Thread   custom thread (must be derived from impl::CallbackThread)
+//
+// Derived must define the following functions:
+//   void Start();
+template <typename Derived, typename Thread>
+class CallbackManager {
+  friend class RpcServerTest;
+
+ public:
+  void Stop() { m_owner.Stop(); }
+
+  void Remove(unsigned int listener_uid) {
+    auto thr = m_owner.GetThread();
+    if (!thr) return;
+    thr->m_listeners.erase(listener_uid);
+  }
+
+  unsigned int CreatePoller() {
+    static_cast<Derived*>(this)->Start();
+    auto thr = m_owner.GetThread();
+    return thr->m_pollers.emplace_back(
+        std::make_shared<typename Thread::Poller>());
+  }
+
+  void RemovePoller(unsigned int poller_uid) {
+    auto thr = m_owner.GetThread();
+    if (!thr) return;
+
+    // Remove any listeners that are associated with this poller
+    for (size_t i = 0; i < thr->m_listeners.size(); ++i) {
+      if (thr->m_listeners[i].poller_uid == poller_uid)
+        thr->m_listeners.erase(i);
+    }
+
+    // Wake up any blocked pollers
+    if (poller_uid >= thr->m_pollers.size()) return;
+    auto poller = thr->m_pollers[poller_uid];
+    if (!poller) return;
+    poller->Terminate();
+    return thr->m_pollers.erase(poller_uid);
+  }
+
+  bool WaitForQueue(double timeout) {
+    auto thr = m_owner.GetThread();
+    if (!thr) return true;
+
+    auto& lock = thr.GetLock();
+    auto timeout_time = std::chrono::steady_clock::now() +
+                        std::chrono::duration<double>(timeout);
+    while (!thr->m_queue.empty()) {
+      if (!thr->m_active) return true;
+      if (timeout == 0) return false;
+      if (timeout < 0) {
+        thr->m_queue_empty.wait(lock);
+      } else {
+        auto cond_timed_out = thr->m_queue_empty.wait_until(lock, timeout_time);
+        if (cond_timed_out == std::cv_status::timeout) return false;
+      }
+    }
+
+    return true;
+  }
+
+  std::vector<typename Thread::UserInfo> Poll(unsigned int poller_uid) {
+    bool timed_out = false;
+    return Poll(poller_uid, -1, &timed_out);
+  }
+
+  std::vector<typename Thread::UserInfo> Poll(unsigned int poller_uid,
+                                              double timeout, bool* timed_out) {
+    std::vector<typename Thread::UserInfo> infos;
+    std::shared_ptr<typename Thread::Poller> poller;
+    {
+      auto thr = m_owner.GetThread();
+      if (!thr) return infos;
+      if (poller_uid > thr->m_pollers.size()) return infos;
+      poller = thr->m_pollers[poller_uid];
+      if (!poller) return infos;
+    }
+
+    std::unique_lock<wpi::mutex> lock(poller->poll_mutex);
+    auto timeout_time = std::chrono::steady_clock::now() +
+                        std::chrono::duration<double>(timeout);
+    *timed_out = false;
+    while (poller->poll_queue.empty()) {
+      if (poller->terminating) return infos;
+      if (poller->cancelling) {
+        // Note: this only works if there's a single thread calling this
+        // function for any particular poller, but that's the intended use.
+        poller->cancelling = false;
+        return infos;
+      }
+      if (timeout == 0) {
+        *timed_out = true;
+        return infos;
+      }
+      if (timeout < 0) {
+        poller->poll_cond.wait(lock);
+      } else {
+        auto cond_timed_out = poller->poll_cond.wait_until(lock, timeout_time);
+        if (cond_timed_out == std::cv_status::timeout) {
+          *timed_out = true;
+          return infos;
+        }
+      }
+    }
+
+    while (!poller->poll_queue.empty()) {
+      infos.emplace_back(std::move(poller->poll_queue.front()));
+      poller->poll_queue.pop();
+    }
+    return infos;
+  }
+
+  void CancelPoll(unsigned int poller_uid) {
+    std::shared_ptr<typename Thread::Poller> poller;
+    {
+      auto thr = m_owner.GetThread();
+      if (!thr) return;
+      if (poller_uid > thr->m_pollers.size()) return;
+      poller = thr->m_pollers[poller_uid];
+      if (!poller) return;
+    }
+
+    {
+      std::lock_guard<wpi::mutex> lock(poller->poll_mutex);
+      poller->cancelling = true;
+    }
+    poller->poll_cond.notify_one();
+  }
+
+ protected:
+  template <typename... Args>
+  void DoStart(Args&&... args) {
+    m_owner.Start(std::forward<Args>(args)...);
+  }
+
+  template <typename... Args>
+  unsigned int DoAdd(Args&&... args) {
+    static_cast<Derived*>(this)->Start();
+    auto thr = m_owner.GetThread();
+    return thr->m_listeners.emplace_back(std::forward<Args>(args)...);
+  }
+
+  template <typename... Args>
+  void Send(unsigned int only_listener, Args&&... args) {
+    auto thr = m_owner.GetThread();
+    if (!thr || thr->m_listeners.empty()) return;
+    thr->m_queue.emplace(std::piecewise_construct,
+                         std::make_tuple(only_listener),
+                         std::forward_as_tuple(std::forward<Args>(args)...));
+    thr->m_cond.notify_one();
+  }
+
+  typename wpi::SafeThreadOwner<Thread>::Proxy GetThread() const {
+    return m_owner.GetThread();
+  }
+
+ private:
+  wpi::SafeThreadOwner<Thread> m_owner;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_CALLBACKMANAGER_H_
diff --git a/ntcore/src/main/native/cpp/ConnectionNotifier.cpp b/ntcore/src/main/native/cpp/ConnectionNotifier.cpp
new file mode 100644
index 0000000..340aad0
--- /dev/null
+++ b/ntcore/src/main/native/cpp/ConnectionNotifier.cpp
@@ -0,0 +1,29 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "ConnectionNotifier.h"
+
+using namespace nt;
+
+ConnectionNotifier::ConnectionNotifier(int inst) : m_inst(inst) {}
+
+void ConnectionNotifier::Start() { DoStart(m_inst); }
+
+unsigned int ConnectionNotifier::Add(
+    std::function<void(const ConnectionNotification& event)> callback) {
+  return DoAdd(callback);
+}
+
+unsigned int ConnectionNotifier::AddPolled(unsigned int poller_uid) {
+  return DoAdd(poller_uid);
+}
+
+void ConnectionNotifier::NotifyConnection(bool connected,
+                                          const ConnectionInfo& conn_info,
+                                          unsigned int only_listener) {
+  Send(only_listener, 0, connected, conn_info);
+}
diff --git a/ntcore/src/main/native/cpp/ConnectionNotifier.h b/ntcore/src/main/native/cpp/ConnectionNotifier.h
new file mode 100644
index 0000000..65eec06
--- /dev/null
+++ b/ntcore/src/main/native/cpp/ConnectionNotifier.h
@@ -0,0 +1,72 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_CONNECTIONNOTIFIER_H_
+#define NTCORE_CONNECTIONNOTIFIER_H_
+
+#include "CallbackManager.h"
+#include "Handle.h"
+#include "IConnectionNotifier.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+namespace impl {
+
+class ConnectionNotifierThread
+    : public CallbackThread<ConnectionNotifierThread, ConnectionNotification> {
+ public:
+  explicit ConnectionNotifierThread(int inst) : m_inst(inst) {}
+
+  bool Matches(const ListenerData& /*listener*/,
+               const ConnectionNotification& /*data*/) {
+    return true;
+  }
+
+  void SetListener(ConnectionNotification* data, unsigned int listener_uid) {
+    data->listener =
+        Handle(m_inst, listener_uid, Handle::kConnectionListener).handle();
+  }
+
+  void DoCallback(
+      std::function<void(const ConnectionNotification& event)> callback,
+      const ConnectionNotification& data) {
+    callback(data);
+  }
+
+  int m_inst;
+};
+
+}  // namespace impl
+
+class ConnectionNotifier
+    : public IConnectionNotifier,
+      public CallbackManager<ConnectionNotifier,
+                             impl::ConnectionNotifierThread> {
+  friend class ConnectionNotifierTest;
+  friend class CallbackManager<ConnectionNotifier,
+                               impl::ConnectionNotifierThread>;
+
+ public:
+  explicit ConnectionNotifier(int inst);
+
+  void Start();
+
+  unsigned int Add(std::function<void(const ConnectionNotification& event)>
+                       callback) override;
+  unsigned int AddPolled(unsigned int poller_uid) override;
+
+  void NotifyConnection(bool connected, const ConnectionInfo& conn_info,
+                        unsigned int only_listener = UINT_MAX) override;
+
+ private:
+  int m_inst;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_CONNECTIONNOTIFIER_H_
diff --git a/ntcore/src/main/native/cpp/Dispatcher.cpp b/ntcore/src/main/native/cpp/Dispatcher.cpp
new file mode 100644
index 0000000..ea54fe4
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Dispatcher.cpp
@@ -0,0 +1,641 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "Dispatcher.h"
+
+#include <algorithm>
+#include <iterator>
+
+#include <wpi/TCPAcceptor.h>
+#include <wpi/TCPConnector.h>
+
+#include "IConnectionNotifier.h"
+#include "IStorage.h"
+#include "Log.h"
+#include "NetworkConnection.h"
+
+using namespace nt;
+
+void Dispatcher::StartServer(const Twine& persist_filename,
+                             const char* listen_address, unsigned int port) {
+  std::string listen_address_copy(StringRef(listen_address).trim());
+  DispatcherBase::StartServer(
+      persist_filename,
+      std::unique_ptr<wpi::NetworkAcceptor>(new wpi::TCPAcceptor(
+          static_cast<int>(port), listen_address_copy.c_str(), m_logger)));
+}
+
+void Dispatcher::SetServer(const char* server_name, unsigned int port) {
+  std::string server_name_copy(StringRef(server_name).trim());
+  SetConnector([=]() -> std::unique_ptr<wpi::NetworkStream> {
+    return wpi::TCPConnector::connect(server_name_copy.c_str(),
+                                      static_cast<int>(port), m_logger, 1);
+  });
+}
+
+void Dispatcher::SetServer(
+    ArrayRef<std::pair<StringRef, unsigned int>> servers) {
+  wpi::SmallVector<std::pair<std::string, int>, 16> servers_copy;
+  for (const auto& server : servers)
+    servers_copy.emplace_back(std::string{server.first.trim()},
+                              static_cast<int>(server.second));
+
+  SetConnector([=]() -> std::unique_ptr<wpi::NetworkStream> {
+    wpi::SmallVector<std::pair<const char*, int>, 16> servers_copy2;
+    for (const auto& server : servers_copy)
+      servers_copy2.emplace_back(server.first.c_str(), server.second);
+    return wpi::TCPConnector::connect_parallel(servers_copy2, m_logger, 1);
+  });
+}
+
+void Dispatcher::SetServerTeam(unsigned int team, unsigned int port) {
+  std::pair<StringRef, unsigned int> servers[5];
+
+  // 10.te.am.2
+  wpi::SmallString<32> fixed;
+  {
+    wpi::raw_svector_ostream oss{fixed};
+    oss << "10." << static_cast<int>(team / 100) << '.'
+        << static_cast<int>(team % 100) << ".2";
+    servers[0] = std::make_pair(oss.str(), port);
+  }
+
+  // 172.22.11.2
+  servers[1] = std::make_pair("172.22.11.2", port);
+
+  // roboRIO-<team>-FRC.local
+  wpi::SmallString<32> mdns;
+  {
+    wpi::raw_svector_ostream oss{mdns};
+    oss << "roboRIO-" << team << "-FRC.local";
+    servers[2] = std::make_pair(oss.str(), port);
+  }
+
+  // roboRIO-<team>-FRC.lan
+  wpi::SmallString<32> mdns_lan;
+  {
+    wpi::raw_svector_ostream oss{mdns_lan};
+    oss << "roboRIO-" << team << "-FRC.lan";
+    servers[3] = std::make_pair(oss.str(), port);
+  }
+
+  // roboRIO-<team>-FRC.frc-field.local
+  wpi::SmallString<64> field_local;
+  {
+    wpi::raw_svector_ostream oss{field_local};
+    oss << "roboRIO-" << team << "-FRC.frc-field.local";
+    servers[4] = std::make_pair(oss.str(), port);
+  }
+
+  SetServer(servers);
+}
+
+void Dispatcher::SetServerOverride(const char* server_name, unsigned int port) {
+  std::string server_name_copy(StringRef(server_name).trim());
+  SetConnectorOverride([=]() -> std::unique_ptr<wpi::NetworkStream> {
+    return wpi::TCPConnector::connect(server_name_copy.c_str(),
+                                      static_cast<int>(port), m_logger, 1);
+  });
+}
+
+void Dispatcher::ClearServerOverride() { ClearConnectorOverride(); }
+
+DispatcherBase::DispatcherBase(IStorage& storage, IConnectionNotifier& notifier,
+                               wpi::Logger& logger)
+    : m_storage(storage), m_notifier(notifier), m_logger(logger) {
+  m_active = false;
+  m_update_rate = 100;
+}
+
+DispatcherBase::~DispatcherBase() { Stop(); }
+
+unsigned int DispatcherBase::GetNetworkMode() const { return m_networkMode; }
+
+void DispatcherBase::StartServer(
+    const Twine& persist_filename,
+    std::unique_ptr<wpi::NetworkAcceptor> acceptor) {
+  {
+    std::lock_guard<wpi::mutex> lock(m_user_mutex);
+    if (m_active) return;
+    m_active = true;
+  }
+  m_networkMode = NT_NET_MODE_SERVER | NT_NET_MODE_STARTING;
+  m_persist_filename = persist_filename.str();
+  m_server_acceptor = std::move(acceptor);
+
+  // Load persistent file.  Ignore errors, but pass along warnings.
+  if (!persist_filename.isTriviallyEmpty() &&
+      (!persist_filename.isSingleStringRef() ||
+       !persist_filename.getSingleStringRef().empty())) {
+    bool first = true;
+    m_storage.LoadPersistent(
+        persist_filename, [&](size_t line, const char* msg) {
+          if (first) {
+            first = false;
+            WARNING("When reading initial persistent values from '"
+                    << persist_filename << "':");
+          }
+          WARNING(persist_filename << ":" << line << ": " << msg);
+        });
+  }
+
+  m_storage.SetDispatcher(this, true);
+
+  m_dispatch_thread = std::thread(&Dispatcher::DispatchThreadMain, this);
+  m_clientserver_thread = std::thread(&Dispatcher::ServerThreadMain, this);
+}
+
+void DispatcherBase::StartClient() {
+  {
+    std::lock_guard<wpi::mutex> lock(m_user_mutex);
+    if (m_active) return;
+    m_active = true;
+  }
+  m_networkMode = NT_NET_MODE_CLIENT | NT_NET_MODE_STARTING;
+  m_storage.SetDispatcher(this, false);
+
+  m_dispatch_thread = std::thread(&Dispatcher::DispatchThreadMain, this);
+  m_clientserver_thread = std::thread(&Dispatcher::ClientThreadMain, this);
+}
+
+void DispatcherBase::Stop() {
+  m_active = false;
+
+  // wake up dispatch thread with a flush
+  m_flush_cv.notify_one();
+
+  // wake up client thread with a reconnect
+  {
+    std::lock_guard<wpi::mutex> lock(m_user_mutex);
+    m_client_connector = nullptr;
+  }
+  ClientReconnect();
+
+  // wake up server thread by shutting down the socket
+  if (m_server_acceptor) m_server_acceptor->shutdown();
+
+  // join threads, with timeout
+  if (m_dispatch_thread.joinable()) m_dispatch_thread.join();
+  if (m_clientserver_thread.joinable()) m_clientserver_thread.join();
+
+  std::vector<std::shared_ptr<INetworkConnection>> conns;
+  {
+    std::lock_guard<wpi::mutex> lock(m_user_mutex);
+    conns.swap(m_connections);
+  }
+
+  // close all connections
+  conns.resize(0);
+}
+
+void DispatcherBase::SetUpdateRate(double interval) {
+  // don't allow update rates faster than 10 ms or slower than 1 second
+  if (interval < 0.01)
+    interval = 0.01;
+  else if (interval > 1.0)
+    interval = 1.0;
+  m_update_rate = static_cast<unsigned int>(interval * 1000);
+}
+
+void DispatcherBase::SetIdentity(const Twine& name) {
+  std::lock_guard<wpi::mutex> lock(m_user_mutex);
+  m_identity = name.str();
+}
+
+void DispatcherBase::Flush() {
+  auto now = std::chrono::steady_clock::now();
+  {
+    std::lock_guard<wpi::mutex> lock(m_flush_mutex);
+    // don't allow flushes more often than every 10 ms
+    if ((now - m_last_flush) < std::chrono::milliseconds(10)) return;
+    m_last_flush = now;
+    m_do_flush = true;
+  }
+  m_flush_cv.notify_one();
+}
+
+std::vector<ConnectionInfo> DispatcherBase::GetConnections() const {
+  std::vector<ConnectionInfo> conns;
+  if (!m_active) return conns;
+
+  std::lock_guard<wpi::mutex> lock(m_user_mutex);
+  for (auto& conn : m_connections) {
+    if (conn->state() != NetworkConnection::kActive) continue;
+    conns.emplace_back(conn->info());
+  }
+
+  return conns;
+}
+
+bool DispatcherBase::IsConnected() const {
+  if (!m_active) return false;
+
+  std::lock_guard<wpi::mutex> lock(m_user_mutex);
+  for (auto& conn : m_connections) {
+    if (conn->state() == NetworkConnection::kActive) return true;
+  }
+
+  return false;
+}
+
+unsigned int DispatcherBase::AddListener(
+    std::function<void(const ConnectionNotification& event)> callback,
+    bool immediate_notify) const {
+  std::lock_guard<wpi::mutex> lock(m_user_mutex);
+  unsigned int uid = m_notifier.Add(callback);
+  // perform immediate notifications
+  if (immediate_notify) {
+    for (auto& conn : m_connections) {
+      if (conn->state() != NetworkConnection::kActive) continue;
+      m_notifier.NotifyConnection(true, conn->info(), uid);
+    }
+  }
+  return uid;
+}
+
+unsigned int DispatcherBase::AddPolledListener(unsigned int poller_uid,
+                                               bool immediate_notify) const {
+  std::lock_guard<wpi::mutex> lock(m_user_mutex);
+  unsigned int uid = m_notifier.AddPolled(poller_uid);
+  // perform immediate notifications
+  if (immediate_notify) {
+    for (auto& conn : m_connections) {
+      if (conn->state() != NetworkConnection::kActive) continue;
+      m_notifier.NotifyConnection(true, conn->info(), uid);
+    }
+  }
+  return uid;
+}
+
+void DispatcherBase::SetConnector(Connector connector) {
+  std::lock_guard<wpi::mutex> lock(m_user_mutex);
+  m_client_connector = std::move(connector);
+}
+
+void DispatcherBase::SetConnectorOverride(Connector connector) {
+  std::lock_guard<wpi::mutex> lock(m_user_mutex);
+  m_client_connector_override = std::move(connector);
+}
+
+void DispatcherBase::ClearConnectorOverride() {
+  std::lock_guard<wpi::mutex> lock(m_user_mutex);
+  m_client_connector_override = nullptr;
+}
+
+void DispatcherBase::DispatchThreadMain() {
+  auto timeout_time = std::chrono::steady_clock::now();
+
+  static const auto save_delta_time = std::chrono::seconds(1);
+  auto next_save_time = timeout_time + save_delta_time;
+
+  int count = 0;
+
+  while (m_active) {
+    // handle loop taking too long
+    auto start = std::chrono::steady_clock::now();
+    if (start > timeout_time) timeout_time = start;
+
+    // wait for periodic or when flushed
+    timeout_time += std::chrono::milliseconds(m_update_rate);
+    std::unique_lock<wpi::mutex> flush_lock(m_flush_mutex);
+    m_flush_cv.wait_until(flush_lock, timeout_time,
+                          [&] { return !m_active || m_do_flush; });
+    m_do_flush = false;
+    flush_lock.unlock();
+    if (!m_active) break;  // in case we were woken up to terminate
+
+    // perform periodic persistent save
+    if ((m_networkMode & NT_NET_MODE_SERVER) != 0 &&
+        !m_persist_filename.empty() && start > next_save_time) {
+      next_save_time += save_delta_time;
+      // handle loop taking too long
+      if (start > next_save_time) next_save_time = start + save_delta_time;
+      const char* err = m_storage.SavePersistent(m_persist_filename, true);
+      if (err) WARNING("periodic persistent save: " << err);
+    }
+
+    {
+      std::lock_guard<wpi::mutex> user_lock(m_user_mutex);
+      bool reconnect = false;
+
+      if (++count > 10) {
+        DEBUG("dispatch running " << m_connections.size() << " connections");
+        count = 0;
+      }
+
+      for (auto& conn : m_connections) {
+        // post outgoing messages if connection is active
+        // only send keep-alives on client
+        if (conn->state() == NetworkConnection::kActive)
+          conn->PostOutgoing((m_networkMode & NT_NET_MODE_CLIENT) != 0);
+
+        // if client, reconnect if connection died
+        if ((m_networkMode & NT_NET_MODE_CLIENT) != 0 &&
+            conn->state() == NetworkConnection::kDead)
+          reconnect = true;
+      }
+      // reconnect if we disconnected (and a reconnect is not in progress)
+      if (reconnect && !m_do_reconnect) {
+        m_do_reconnect = true;
+        m_reconnect_cv.notify_one();
+      }
+    }
+  }
+}
+
+void DispatcherBase::QueueOutgoing(std::shared_ptr<Message> msg,
+                                   INetworkConnection* only,
+                                   INetworkConnection* except) {
+  std::lock_guard<wpi::mutex> user_lock(m_user_mutex);
+  for (auto& conn : m_connections) {
+    if (conn.get() == except) continue;
+    if (only && conn.get() != only) continue;
+    auto state = conn->state();
+    if (state != NetworkConnection::kSynchronized &&
+        state != NetworkConnection::kActive)
+      continue;
+    conn->QueueOutgoing(msg);
+  }
+}
+
+void DispatcherBase::ServerThreadMain() {
+  if (m_server_acceptor->start() != 0) {
+    m_active = false;
+    m_networkMode = NT_NET_MODE_SERVER | NT_NET_MODE_FAILURE;
+    return;
+  }
+  m_networkMode = NT_NET_MODE_SERVER;
+  while (m_active) {
+    auto stream = m_server_acceptor->accept();
+    if (!stream) {
+      m_active = false;
+      return;
+    }
+    if (!m_active) {
+      m_networkMode = NT_NET_MODE_NONE;
+      return;
+    }
+    DEBUG("server: client connection from " << stream->getPeerIP() << " port "
+                                            << stream->getPeerPort());
+
+    // add to connections list
+    using namespace std::placeholders;
+    auto conn = std::make_shared<NetworkConnection>(
+        ++m_connections_uid, std::move(stream), m_notifier, m_logger,
+        std::bind(&Dispatcher::ServerHandshake, this, _1, _2, _3),
+        std::bind(&IStorage::GetMessageEntryType, &m_storage, _1));
+    conn->set_process_incoming(
+        std::bind(&IStorage::ProcessIncoming, &m_storage, _1, _2,
+                  std::weak_ptr<NetworkConnection>(conn)));
+    {
+      std::lock_guard<wpi::mutex> lock(m_user_mutex);
+      // reuse dead connection slots
+      bool placed = false;
+      for (auto& c : m_connections) {
+        if (c->state() == NetworkConnection::kDead) {
+          c = conn;
+          placed = true;
+          break;
+        }
+      }
+      if (!placed) m_connections.emplace_back(conn);
+      conn->Start();
+    }
+  }
+  m_networkMode = NT_NET_MODE_NONE;
+}
+
+void DispatcherBase::ClientThreadMain() {
+  while (m_active) {
+    // sleep between retries
+    std::this_thread::sleep_for(std::chrono::milliseconds(250));
+    Connector connect;
+
+    // get next server to connect to
+    {
+      std::lock_guard<wpi::mutex> lock(m_user_mutex);
+      if (m_client_connector_override) {
+        connect = m_client_connector_override;
+      } else {
+        if (!m_client_connector) {
+          m_networkMode = NT_NET_MODE_CLIENT | NT_NET_MODE_FAILURE;
+          continue;
+        }
+        connect = m_client_connector;
+      }
+    }
+
+    // try to connect (with timeout)
+    DEBUG("client trying to connect");
+    auto stream = connect();
+    if (!stream) {
+      m_networkMode = NT_NET_MODE_CLIENT | NT_NET_MODE_FAILURE;
+      continue;  // keep retrying
+    }
+    DEBUG("client connected");
+    m_networkMode = NT_NET_MODE_CLIENT;
+
+    std::unique_lock<wpi::mutex> lock(m_user_mutex);
+    using namespace std::placeholders;
+    auto conn = std::make_shared<NetworkConnection>(
+        ++m_connections_uid, std::move(stream), m_notifier, m_logger,
+        std::bind(&Dispatcher::ClientHandshake, this, _1, _2, _3),
+        std::bind(&IStorage::GetMessageEntryType, &m_storage, _1));
+    conn->set_process_incoming(
+        std::bind(&IStorage::ProcessIncoming, &m_storage, _1, _2,
+                  std::weak_ptr<NetworkConnection>(conn)));
+    m_connections.resize(0);  // disconnect any current
+    m_connections.emplace_back(conn);
+    conn->set_proto_rev(m_reconnect_proto_rev);
+    conn->Start();
+
+    // reconnect the next time starting with latest protocol revision
+    m_reconnect_proto_rev = 0x0300;
+
+    // block until told to reconnect
+    m_do_reconnect = false;
+    m_reconnect_cv.wait(lock, [&] { return !m_active || m_do_reconnect; });
+  }
+  m_networkMode = NT_NET_MODE_NONE;
+}
+
+bool DispatcherBase::ClientHandshake(
+    NetworkConnection& conn, std::function<std::shared_ptr<Message>()> get_msg,
+    std::function<void(wpi::ArrayRef<std::shared_ptr<Message>>)> send_msgs) {
+  // get identity
+  std::string self_id;
+  {
+    std::lock_guard<wpi::mutex> lock(m_user_mutex);
+    self_id = m_identity;
+  }
+
+  // send client hello
+  DEBUG("client: sending hello");
+  send_msgs(Message::ClientHello(self_id));
+
+  // wait for response
+  auto msg = get_msg();
+  if (!msg) {
+    // disconnected, retry
+    DEBUG("client: server disconnected before first response");
+    return false;
+  }
+
+  if (msg->Is(Message::kProtoUnsup)) {
+    if (msg->id() == 0x0200) ClientReconnect(0x0200);
+    return false;
+  }
+
+  bool new_server = true;
+  if (conn.proto_rev() >= 0x0300) {
+    // should be server hello; if not, disconnect.
+    if (!msg->Is(Message::kServerHello)) return false;
+    conn.set_remote_id(msg->str());
+    if ((msg->flags() & 1) != 0) new_server = false;
+    // get the next message
+    msg = get_msg();
+  }
+
+  // receive initial assignments
+  std::vector<std::shared_ptr<Message>> incoming;
+  for (;;) {
+    if (!msg) {
+      // disconnected, retry
+      DEBUG("client: server disconnected during initial entries");
+      return false;
+    }
+    DEBUG4("received init str=" << msg->str() << " id=" << msg->id()
+                                << " seq_num=" << msg->seq_num_uid());
+    if (msg->Is(Message::kServerHelloDone)) break;
+    // shouldn't receive a keep alive, but handle gracefully
+    if (msg->Is(Message::kKeepAlive)) {
+      msg = get_msg();
+      continue;
+    }
+    if (!msg->Is(Message::kEntryAssign)) {
+      // unexpected message
+      DEBUG("client: received message ("
+            << msg->type()
+            << ") other than entry assignment during initial handshake");
+      return false;
+    }
+    incoming.emplace_back(std::move(msg));
+    // get the next message
+    msg = get_msg();
+  }
+
+  // generate outgoing assignments
+  NetworkConnection::Outgoing outgoing;
+
+  m_storage.ApplyInitialAssignments(conn, incoming, new_server, &outgoing);
+
+  if (conn.proto_rev() >= 0x0300)
+    outgoing.emplace_back(Message::ClientHelloDone());
+
+  if (!outgoing.empty()) send_msgs(outgoing);
+
+  INFO("client: CONNECTED to server " << conn.stream().getPeerIP() << " port "
+                                      << conn.stream().getPeerPort());
+  return true;
+}
+
+bool DispatcherBase::ServerHandshake(
+    NetworkConnection& conn, std::function<std::shared_ptr<Message>()> get_msg,
+    std::function<void(wpi::ArrayRef<std::shared_ptr<Message>>)> send_msgs) {
+  // Wait for the client to send us a hello.
+  auto msg = get_msg();
+  if (!msg) {
+    DEBUG("server: client disconnected before sending hello");
+    return false;
+  }
+  if (!msg->Is(Message::kClientHello)) {
+    DEBUG("server: client initial message was not client hello");
+    return false;
+  }
+
+  // Check that the client requested version is not too high.
+  unsigned int proto_rev = msg->id();
+  if (proto_rev > 0x0300) {
+    DEBUG("server: client requested proto > 0x0300");
+    send_msgs(Message::ProtoUnsup());
+    return false;
+  }
+
+  if (proto_rev >= 0x0300) conn.set_remote_id(msg->str());
+
+  // Set the proto version to the client requested version
+  DEBUG("server: client protocol " << proto_rev);
+  conn.set_proto_rev(proto_rev);
+
+  // Send initial set of assignments
+  NetworkConnection::Outgoing outgoing;
+
+  // Start with server hello.  TODO: initial connection flag
+  if (proto_rev >= 0x0300) {
+    std::lock_guard<wpi::mutex> lock(m_user_mutex);
+    outgoing.emplace_back(Message::ServerHello(0u, m_identity));
+  }
+
+  // Get snapshot of initial assignments
+  m_storage.GetInitialAssignments(conn, &outgoing);
+
+  // Finish with server hello done
+  outgoing.emplace_back(Message::ServerHelloDone());
+
+  // Batch transmit
+  DEBUG("server: sending initial assignments");
+  send_msgs(outgoing);
+
+  // In proto rev 3.0 and later, the handshake concludes with a client hello
+  // done message, so we can batch the assigns before marking the connection
+  // active.  In pre-3.0, we need to just immediately mark it active and hand
+  // off control to the dispatcher to assign them as they arrive.
+  if (proto_rev >= 0x0300) {
+    // receive client initial assignments
+    std::vector<std::shared_ptr<Message>> incoming;
+    msg = get_msg();
+    for (;;) {
+      if (!msg) {
+        // disconnected, retry
+        DEBUG("server: disconnected waiting for initial entries");
+        return false;
+      }
+      if (msg->Is(Message::kClientHelloDone)) break;
+      // shouldn't receive a keep alive, but handle gracefully
+      if (msg->Is(Message::kKeepAlive)) {
+        msg = get_msg();
+        continue;
+      }
+      if (!msg->Is(Message::kEntryAssign)) {
+        // unexpected message
+        DEBUG("server: received message ("
+              << msg->type()
+              << ") other than entry assignment during initial handshake");
+        return false;
+      }
+      incoming.push_back(msg);
+      // get the next message (blocks)
+      msg = get_msg();
+    }
+    for (auto& msg : incoming)
+      m_storage.ProcessIncoming(msg, &conn, std::weak_ptr<NetworkConnection>());
+  }
+
+  INFO("server: client CONNECTED: " << conn.stream().getPeerIP() << " port "
+                                    << conn.stream().getPeerPort());
+  return true;
+}
+
+void DispatcherBase::ClientReconnect(unsigned int proto_rev) {
+  if ((m_networkMode & NT_NET_MODE_SERVER) != 0) return;
+  {
+    std::lock_guard<wpi::mutex> lock(m_user_mutex);
+    m_reconnect_proto_rev = proto_rev;
+    m_do_reconnect = true;
+  }
+  m_reconnect_cv.notify_one();
+}
diff --git a/ntcore/src/main/native/cpp/Dispatcher.h b/ntcore/src/main/native/cpp/Dispatcher.h
new file mode 100644
index 0000000..dfaf4d5
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Dispatcher.h
@@ -0,0 +1,151 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_DISPATCHER_H_
+#define NTCORE_DISPATCHER_H_
+
+#include <atomic>
+#include <chrono>
+#include <functional>
+#include <memory>
+#include <string>
+#include <thread>
+#include <utility>
+#include <vector>
+
+#include <wpi/StringRef.h>
+#include <wpi/Twine.h>
+#include <wpi/condition_variable.h>
+#include <wpi/mutex.h>
+
+#include "IDispatcher.h"
+#include "INetworkConnection.h"
+
+namespace wpi {
+class Logger;
+class NetworkAcceptor;
+class NetworkStream;
+}  // namespace wpi
+
+namespace nt {
+
+class IConnectionNotifier;
+class IStorage;
+class NetworkConnection;
+
+class DispatcherBase : public IDispatcher {
+  friend class DispatcherTest;
+
+ public:
+  typedef std::function<std::unique_ptr<wpi::NetworkStream>()> Connector;
+
+  DispatcherBase(IStorage& storage, IConnectionNotifier& notifier,
+                 wpi::Logger& logger);
+  virtual ~DispatcherBase();
+
+  unsigned int GetNetworkMode() const;
+  void StartServer(const Twine& persist_filename,
+                   std::unique_ptr<wpi::NetworkAcceptor> acceptor);
+  void StartClient();
+  void Stop();
+  void SetUpdateRate(double interval);
+  void SetIdentity(const Twine& name);
+  void Flush();
+  std::vector<ConnectionInfo> GetConnections() const;
+  bool IsConnected() const;
+
+  unsigned int AddListener(
+      std::function<void(const ConnectionNotification& event)> callback,
+      bool immediate_notify) const;
+  unsigned int AddPolledListener(unsigned int poller_uid,
+                                 bool immediate_notify) const;
+
+  void SetConnector(Connector connector);
+  void SetConnectorOverride(Connector connector);
+  void ClearConnectorOverride();
+
+  bool active() const { return m_active; }
+
+  DispatcherBase(const DispatcherBase&) = delete;
+  DispatcherBase& operator=(const DispatcherBase&) = delete;
+
+ private:
+  void DispatchThreadMain();
+  void ServerThreadMain();
+  void ClientThreadMain();
+
+  bool ClientHandshake(
+      NetworkConnection& conn,
+      std::function<std::shared_ptr<Message>()> get_msg,
+      std::function<void(wpi::ArrayRef<std::shared_ptr<Message>>)> send_msgs);
+  bool ServerHandshake(
+      NetworkConnection& conn,
+      std::function<std::shared_ptr<Message>()> get_msg,
+      std::function<void(wpi::ArrayRef<std::shared_ptr<Message>>)> send_msgs);
+
+  void ClientReconnect(unsigned int proto_rev = 0x0300);
+
+  void QueueOutgoing(std::shared_ptr<Message> msg, INetworkConnection* only,
+                     INetworkConnection* except) override;
+
+  IStorage& m_storage;
+  IConnectionNotifier& m_notifier;
+  unsigned int m_networkMode = NT_NET_MODE_NONE;
+  std::string m_persist_filename;
+  std::thread m_dispatch_thread;
+  std::thread m_clientserver_thread;
+
+  std::unique_ptr<wpi::NetworkAcceptor> m_server_acceptor;
+  Connector m_client_connector_override;
+  Connector m_client_connector;
+  uint8_t m_connections_uid = 0;
+
+  // Mutex for user-accessible items
+  mutable wpi::mutex m_user_mutex;
+  std::vector<std::shared_ptr<INetworkConnection>> m_connections;
+  std::string m_identity;
+
+  std::atomic_bool m_active;       // set to false to terminate threads
+  std::atomic_uint m_update_rate;  // periodic dispatch update rate, in ms
+
+  // Condition variable for forced dispatch wakeup (flush)
+  wpi::mutex m_flush_mutex;
+  wpi::condition_variable m_flush_cv;
+  std::chrono::steady_clock::time_point m_last_flush;
+  bool m_do_flush = false;
+
+  // Condition variable for client reconnect (uses user mutex)
+  wpi::condition_variable m_reconnect_cv;
+  unsigned int m_reconnect_proto_rev = 0x0300;
+  bool m_do_reconnect = true;
+
+ protected:
+  wpi::Logger& m_logger;
+};
+
+class Dispatcher : public DispatcherBase {
+  friend class DispatcherTest;
+
+ public:
+  Dispatcher(IStorage& storage, IConnectionNotifier& notifier,
+             wpi::Logger& logger)
+      : DispatcherBase(storage, notifier, logger) {}
+
+  void StartServer(const Twine& persist_filename, const char* listen_address,
+                   unsigned int port);
+
+  void SetServer(const char* server_name, unsigned int port);
+  void SetServer(ArrayRef<std::pair<StringRef, unsigned int>> servers);
+  void SetServerTeam(unsigned int team, unsigned int port);
+
+  void SetServerOverride(const char* server_name, unsigned int port);
+  void ClearServerOverride();
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_DISPATCHER_H_
diff --git a/ntcore/src/main/native/cpp/DsClient.cpp b/ntcore/src/main/native/cpp/DsClient.cpp
new file mode 100644
index 0000000..8b97be7
--- /dev/null
+++ b/ntcore/src/main/native/cpp/DsClient.cpp
@@ -0,0 +1,153 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "DsClient.h"
+
+#include <wpi/SmallString.h>
+#include <wpi/TCPConnector.h>
+#include <wpi/raw_ostream.h>
+#include <wpi/raw_socket_istream.h>
+
+#include "Dispatcher.h"
+#include "Log.h"
+
+using namespace nt;
+
+class DsClient::Thread : public wpi::SafeThread {
+ public:
+  Thread(Dispatcher& dispatcher, wpi::Logger& logger, unsigned int port)
+      : m_dispatcher(dispatcher), m_logger(logger), m_port(port) {}
+
+  void Main();
+
+  Dispatcher& m_dispatcher;
+  wpi::Logger& m_logger;
+  unsigned int m_port;
+  std::unique_ptr<wpi::NetworkStream> m_stream;
+};
+
+DsClient::DsClient(Dispatcher& dispatcher, wpi::Logger& logger)
+    : m_dispatcher(dispatcher), m_logger(logger) {}
+
+void DsClient::Start(unsigned int port) {
+  auto thr = m_owner.GetThread();
+  if (!thr)
+    m_owner.Start(m_dispatcher, m_logger, port);
+  else
+    thr->m_port = port;
+}
+
+void DsClient::Stop() {
+  {
+    // Close the stream so the read (if any) terminates.
+    auto thr = m_owner.GetThread();
+    if (thr) {
+      thr->m_active = false;
+      if (thr->m_stream) thr->m_stream->close();
+    }
+  }
+  m_owner.Stop();
+}
+
+void DsClient::Thread::Main() {
+  unsigned int oldip = 0;
+  wpi::Logger nolog;  // to silence log messages from TCPConnector
+
+  while (m_active) {
+    // wait for periodic reconnect or termination
+    auto timeout_time =
+        std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
+    unsigned int port;
+    {
+      std::unique_lock<wpi::mutex> lock(m_mutex);
+      m_cond.wait_until(lock, timeout_time, [&] { return !m_active; });
+      port = m_port;
+    }
+    if (!m_active) goto done;
+
+    // Try to connect to DS on the local machine
+    m_stream = wpi::TCPConnector::connect("127.0.0.1", 1742, nolog, 1);
+    if (!m_active) goto done;
+    if (!m_stream) continue;
+
+    DEBUG3("connected to DS");
+    wpi::raw_socket_istream is(*m_stream);
+
+    while (m_active && !is.has_error()) {
+      // Read JSON "{...}".  This is very limited, does not handle quoted "}" or
+      // nested {}, but is sufficient for this purpose.
+      wpi::SmallString<128> json;
+      char ch;
+
+      // Throw away characters until {
+      do {
+        is.read(ch);
+        if (is.has_error()) break;
+        if (!m_active) goto done;
+      } while (ch != '{');
+      json += '{';
+
+      if (is.has_error()) {
+        m_stream = nullptr;
+        break;
+      }
+
+      // Read characters until }
+      do {
+        is.read(ch);
+        if (is.has_error()) break;
+        if (!m_active) goto done;
+        json += ch;
+      } while (ch != '}');
+
+      if (is.has_error()) {
+        m_stream = nullptr;
+        break;
+      }
+      DEBUG3("json=" << json);
+
+      // Look for "robotIP":12345, and get 12345 portion
+      size_t pos = json.find("\"robotIP\"");
+      if (pos == wpi::StringRef::npos) continue;  // could not find?
+      pos += 9;
+      pos = json.find(':', pos);
+      if (pos == wpi::StringRef::npos) continue;  // could not find?
+      size_t endpos = json.find_first_not_of("0123456789", pos + 1);
+      DEBUG3("found robotIP=" << json.slice(pos + 1, endpos));
+
+      // Parse into number
+      unsigned int ip = 0;
+      if (json.slice(pos + 1, endpos).getAsInteger(10, ip)) continue;  // error
+
+      // If zero, clear the server override
+      if (ip == 0) {
+        m_dispatcher.ClearServerOverride();
+        oldip = 0;
+        continue;
+      }
+
+      // If unchanged, don't reconnect
+      if (ip == oldip) continue;
+      oldip = ip;
+
+      // Convert number into dotted quad
+      json.clear();
+      wpi::raw_svector_ostream os{json};
+      os << ((ip >> 24) & 0xff) << "." << ((ip >> 16) & 0xff) << "."
+         << ((ip >> 8) & 0xff) << "." << (ip & 0xff);
+      INFO("client: DS overriding server IP to " << os.str());
+      m_dispatcher.SetServerOverride(json.c_str(), port);
+    }
+
+    // We disconnected from the DS, clear the server override
+    m_dispatcher.ClearServerOverride();
+    oldip = 0;
+  }
+
+done:
+  m_dispatcher.ClearServerOverride();
+}
diff --git a/ntcore/src/main/native/cpp/DsClient.h b/ntcore/src/main/native/cpp/DsClient.h
new file mode 100644
index 0000000..ad13d25
--- /dev/null
+++ b/ntcore/src/main/native/cpp/DsClient.h
@@ -0,0 +1,36 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_DSCLIENT_H_
+#define NTCORE_DSCLIENT_H_
+
+#include <wpi/SafeThread.h>
+
+#include "Log.h"
+
+namespace nt {
+
+class Dispatcher;
+
+class DsClient {
+ public:
+  DsClient(Dispatcher& dispatcher, wpi::Logger& logger);
+  ~DsClient() = default;
+
+  void Start(unsigned int port);
+  void Stop();
+
+ private:
+  class Thread;
+  wpi::SafeThreadOwner<Thread> m_owner;
+  Dispatcher& m_dispatcher;
+  wpi::Logger& m_logger;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_DSCLIENT_H_
diff --git a/ntcore/src/main/native/cpp/EntryNotifier.cpp b/ntcore/src/main/native/cpp/EntryNotifier.cpp
new file mode 100644
index 0000000..5fde687
--- /dev/null
+++ b/ntcore/src/main/native/cpp/EntryNotifier.cpp
@@ -0,0 +1,89 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "EntryNotifier.h"
+
+#include "Log.h"
+
+using namespace nt;
+
+EntryNotifier::EntryNotifier(int inst, wpi::Logger& logger)
+    : m_inst(inst), m_logger(logger) {
+  m_local_notifiers = false;
+}
+
+void EntryNotifier::Start() { DoStart(m_inst); }
+
+bool EntryNotifier::local_notifiers() const { return m_local_notifiers; }
+
+bool impl::EntryNotifierThread::Matches(const EntryListenerData& listener,
+                                        const EntryNotification& data) {
+  if (!data.value) return false;
+
+  // Flags must be within requested flag set for this listener.
+  // Because assign messages can result in both a value and flags update,
+  // we handle that case specially.
+  unsigned int listen_flags =
+      listener.flags & ~(NT_NOTIFY_IMMEDIATE | NT_NOTIFY_LOCAL);
+  unsigned int flags = data.flags & ~(NT_NOTIFY_IMMEDIATE | NT_NOTIFY_LOCAL);
+  unsigned int assign_both = NT_NOTIFY_UPDATE | NT_NOTIFY_FLAGS;
+  if ((flags & assign_both) == assign_both) {
+    if ((listen_flags & assign_both) == 0) return false;
+    listen_flags &= ~assign_both;
+    flags &= ~assign_both;
+  }
+  if ((flags & ~listen_flags) != 0) return false;
+
+  // must match local id or prefix
+  if (listener.entry != 0 && data.entry != listener.entry) return false;
+  if (listener.entry == 0 &&
+      !wpi::StringRef(data.name).startswith(listener.prefix))
+    return false;
+
+  return true;
+}
+
+unsigned int EntryNotifier::Add(
+    std::function<void(const EntryNotification& event)> callback,
+    StringRef prefix, unsigned int flags) {
+  if ((flags & NT_NOTIFY_LOCAL) != 0) m_local_notifiers = true;
+  return DoAdd(callback, prefix, flags);
+}
+
+unsigned int EntryNotifier::Add(
+    std::function<void(const EntryNotification& event)> callback,
+    unsigned int local_id, unsigned int flags) {
+  if ((flags & NT_NOTIFY_LOCAL) != 0) m_local_notifiers = true;
+  return DoAdd(callback, Handle(m_inst, local_id, Handle::kEntry), flags);
+}
+
+unsigned int EntryNotifier::AddPolled(unsigned int poller_uid,
+                                      wpi::StringRef prefix,
+                                      unsigned int flags) {
+  if ((flags & NT_NOTIFY_LOCAL) != 0) m_local_notifiers = true;
+  return DoAdd(poller_uid, prefix, flags);
+}
+
+unsigned int EntryNotifier::AddPolled(unsigned int poller_uid,
+                                      unsigned int local_id,
+                                      unsigned int flags) {
+  if ((flags & NT_NOTIFY_LOCAL) != 0) m_local_notifiers = true;
+  return DoAdd(poller_uid, Handle(m_inst, local_id, Handle::kEntry), flags);
+}
+
+void EntryNotifier::NotifyEntry(unsigned int local_id, StringRef name,
+                                std::shared_ptr<Value> value,
+                                unsigned int flags,
+                                unsigned int only_listener) {
+  // optimization: don't generate needless local queue entries if we have
+  // no local listeners (as this is a common case on the server side)
+  if ((flags & NT_NOTIFY_LOCAL) != 0 && !m_local_notifiers) return;
+  DEBUG("notifying '" << name << "' (local=" << local_id
+                      << "), flags=" << flags);
+  Send(only_listener, 0, Handle(m_inst, local_id, Handle::kEntry).handle(),
+       name, value, flags);
+}
diff --git a/ntcore/src/main/native/cpp/EntryNotifier.h b/ntcore/src/main/native/cpp/EntryNotifier.h
new file mode 100644
index 0000000..3ccf9ff
--- /dev/null
+++ b/ntcore/src/main/native/cpp/EntryNotifier.h
@@ -0,0 +1,109 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_ENTRYNOTIFIER_H_
+#define NTCORE_ENTRYNOTIFIER_H_
+
+#include <atomic>
+#include <memory>
+#include <string>
+
+#include "CallbackManager.h"
+#include "Handle.h"
+#include "IEntryNotifier.h"
+#include "ntcore_cpp.h"
+
+namespace wpi {
+class Logger;
+}  // namespace wpi
+
+namespace nt {
+
+namespace impl {
+
+struct EntryListenerData
+    : public ListenerData<std::function<void(const EntryNotification& event)>> {
+  EntryListenerData() = default;
+  EntryListenerData(
+      std::function<void(const EntryNotification& event)> callback_,
+      StringRef prefix_, unsigned int flags_)
+      : ListenerData(callback_), prefix(prefix_), flags(flags_) {}
+  EntryListenerData(
+      std::function<void(const EntryNotification& event)> callback_,
+      NT_Entry entry_, unsigned int flags_)
+      : ListenerData(callback_), entry(entry_), flags(flags_) {}
+  EntryListenerData(unsigned int poller_uid_, StringRef prefix_,
+                    unsigned int flags_)
+      : ListenerData(poller_uid_), prefix(prefix_), flags(flags_) {}
+  EntryListenerData(unsigned int poller_uid_, NT_Entry entry_,
+                    unsigned int flags_)
+      : ListenerData(poller_uid_), entry(entry_), flags(flags_) {}
+
+  std::string prefix;
+  NT_Entry entry = 0;
+  unsigned int flags;
+};
+
+class EntryNotifierThread
+    : public CallbackThread<EntryNotifierThread, EntryNotification,
+                            EntryListenerData> {
+ public:
+  explicit EntryNotifierThread(int inst) : m_inst(inst) {}
+
+  bool Matches(const EntryListenerData& listener,
+               const EntryNotification& data);
+
+  void SetListener(EntryNotification* data, unsigned int listener_uid) {
+    data->listener =
+        Handle(m_inst, listener_uid, Handle::kEntryListener).handle();
+  }
+
+  void DoCallback(std::function<void(const EntryNotification& event)> callback,
+                  const EntryNotification& data) {
+    callback(data);
+  }
+
+  int m_inst;
+};
+
+}  // namespace impl
+
+class EntryNotifier
+    : public IEntryNotifier,
+      public CallbackManager<EntryNotifier, impl::EntryNotifierThread> {
+  friend class EntryNotifierTest;
+  friend class CallbackManager<EntryNotifier, impl::EntryNotifierThread>;
+
+ public:
+  explicit EntryNotifier(int inst, wpi::Logger& logger);
+
+  void Start();
+
+  bool local_notifiers() const override;
+
+  unsigned int Add(std::function<void(const EntryNotification& event)> callback,
+                   wpi::StringRef prefix, unsigned int flags) override;
+  unsigned int Add(std::function<void(const EntryNotification& event)> callback,
+                   unsigned int local_id, unsigned int flags) override;
+  unsigned int AddPolled(unsigned int poller_uid, wpi::StringRef prefix,
+                         unsigned int flags) override;
+  unsigned int AddPolled(unsigned int poller_uid, unsigned int local_id,
+                         unsigned int flags) override;
+
+  void NotifyEntry(unsigned int local_id, StringRef name,
+                   std::shared_ptr<Value> value, unsigned int flags,
+                   unsigned int only_listener = UINT_MAX) override;
+
+ private:
+  int m_inst;
+  wpi::Logger& m_logger;
+  std::atomic_bool m_local_notifiers;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_ENTRYNOTIFIER_H_
diff --git a/ntcore/src/main/native/cpp/Handle.h b/ntcore/src/main/native/cpp/Handle.h
new file mode 100644
index 0000000..47b5edf
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Handle.h
@@ -0,0 +1,65 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_HANDLE_H_
+#define NTCORE_HANDLE_H_
+
+#include "ntcore_c.h"
+
+namespace nt {
+
+// Handle data layout:
+// Bits 30-28: Type
+// Bits 27-20: Instance index
+// Bits 19-0:  Handle index (0/unused for instance handles)
+
+class Handle {
+ public:
+  enum Type {
+    kConnectionListener = 1,
+    kConnectionListenerPoller,
+    kEntry,
+    kEntryListener,
+    kEntryListenerPoller,
+    kInstance,
+    kLogger,
+    kLoggerPoller,
+    kRpcCall,
+    kRpcCallPoller
+  };
+  enum { kIndexMax = 0xfffff };
+
+  explicit Handle(NT_Handle handle) : m_handle(handle) {}
+  operator NT_Handle() const { return m_handle; }
+
+  NT_Handle handle() const { return m_handle; }
+
+  Handle(int inst, int index, Type type) {
+    if (inst < 0 || index < 0) {
+      m_handle = 0;
+      return;
+    }
+    m_handle = ((static_cast<int>(type) & 0xf) << 27) | ((inst & 0x7f) << 20) |
+               (index & 0xfffff);
+  }
+
+  int GetIndex() const { return static_cast<int>(m_handle) & 0xfffff; }
+  Type GetType() const {
+    return static_cast<Type>((static_cast<int>(m_handle) >> 27) & 0xf);
+  }
+  int GetInst() const { return (static_cast<int>(m_handle) >> 20) & 0x7f; }
+  bool IsType(Type type) const { return type == GetType(); }
+  int GetTypedIndex(Type type) const { return IsType(type) ? GetIndex() : -1; }
+  int GetTypedInst(Type type) const { return IsType(type) ? GetInst() : -1; }
+
+ private:
+  NT_Handle m_handle;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_HANDLE_H_
diff --git a/ntcore/src/main/native/cpp/IConnectionNotifier.h b/ntcore/src/main/native/cpp/IConnectionNotifier.h
new file mode 100644
index 0000000..7a165f2
--- /dev/null
+++ b/ntcore/src/main/native/cpp/IConnectionNotifier.h
@@ -0,0 +1,32 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_ICONNECTIONNOTIFIER_H_
+#define NTCORE_ICONNECTIONNOTIFIER_H_
+
+#include <climits>
+
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+class IConnectionNotifier {
+ public:
+  IConnectionNotifier() = default;
+  IConnectionNotifier(const IConnectionNotifier&) = delete;
+  IConnectionNotifier& operator=(const IConnectionNotifier&) = delete;
+  virtual ~IConnectionNotifier() = default;
+  virtual unsigned int Add(
+      std::function<void(const ConnectionNotification& event)> callback) = 0;
+  virtual unsigned int AddPolled(unsigned int poller_uid) = 0;
+  virtual void NotifyConnection(bool connected, const ConnectionInfo& conn_info,
+                                unsigned int only_listener = UINT_MAX) = 0;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_ICONNECTIONNOTIFIER_H_
diff --git a/ntcore/src/main/native/cpp/IDispatcher.h b/ntcore/src/main/native/cpp/IDispatcher.h
new file mode 100644
index 0000000..aace766
--- /dev/null
+++ b/ntcore/src/main/native/cpp/IDispatcher.h
@@ -0,0 +1,34 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_IDISPATCHER_H_
+#define NTCORE_IDISPATCHER_H_
+
+#include <memory>
+
+#include "Message.h"
+
+namespace nt {
+
+class INetworkConnection;
+
+// Interface for generation of outgoing messages to break a dependency loop
+// between Storage and Dispatcher.
+class IDispatcher {
+ public:
+  IDispatcher() = default;
+  IDispatcher(const IDispatcher&) = delete;
+  IDispatcher& operator=(const IDispatcher&) = delete;
+  virtual ~IDispatcher() = default;
+  virtual void QueueOutgoing(std::shared_ptr<Message> msg,
+                             INetworkConnection* only,
+                             INetworkConnection* except) = 0;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_IDISPATCHER_H_
diff --git a/ntcore/src/main/native/cpp/IEntryNotifier.h b/ntcore/src/main/native/cpp/IEntryNotifier.h
new file mode 100644
index 0000000..80ed97e
--- /dev/null
+++ b/ntcore/src/main/native/cpp/IEntryNotifier.h
@@ -0,0 +1,44 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_IENTRYNOTIFIER_H_
+#define NTCORE_IENTRYNOTIFIER_H_
+
+#include <climits>
+#include <memory>
+
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+class IEntryNotifier {
+ public:
+  IEntryNotifier() = default;
+  IEntryNotifier(const IEntryNotifier&) = delete;
+  IEntryNotifier& operator=(const IEntryNotifier&) = delete;
+  virtual ~IEntryNotifier() = default;
+  virtual bool local_notifiers() const = 0;
+
+  virtual unsigned int Add(
+      std::function<void(const EntryNotification& event)> callback,
+      wpi::StringRef prefix, unsigned int flags) = 0;
+  virtual unsigned int Add(
+      std::function<void(const EntryNotification& event)> callback,
+      unsigned int local_id, unsigned int flags) = 0;
+  virtual unsigned int AddPolled(unsigned int poller_uid, wpi::StringRef prefix,
+                                 unsigned int flags) = 0;
+  virtual unsigned int AddPolled(unsigned int poller_uid, unsigned int local_id,
+                                 unsigned int flags) = 0;
+
+  virtual void NotifyEntry(unsigned int local_id, StringRef name,
+                           std::shared_ptr<Value> value, unsigned int flags,
+                           unsigned int only_listener = UINT_MAX) = 0;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_IENTRYNOTIFIER_H_
diff --git a/ntcore/src/main/native/cpp/INetworkConnection.h b/ntcore/src/main/native/cpp/INetworkConnection.h
new file mode 100644
index 0000000..0387cc9
--- /dev/null
+++ b/ntcore/src/main/native/cpp/INetworkConnection.h
@@ -0,0 +1,41 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_INETWORKCONNECTION_H_
+#define NTCORE_INETWORKCONNECTION_H_
+
+#include <memory>
+
+#include "Message.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+class INetworkConnection {
+ public:
+  enum State { kCreated, kInit, kHandshake, kSynchronized, kActive, kDead };
+
+  INetworkConnection() = default;
+  INetworkConnection(const INetworkConnection&) = delete;
+  INetworkConnection& operator=(const INetworkConnection&) = delete;
+  virtual ~INetworkConnection() = default;
+
+  virtual ConnectionInfo info() const = 0;
+
+  virtual void QueueOutgoing(std::shared_ptr<Message> msg) = 0;
+  virtual void PostOutgoing(bool keep_alive) = 0;
+
+  virtual unsigned int proto_rev() const = 0;
+  virtual void set_proto_rev(unsigned int proto_rev) = 0;
+
+  virtual State state() const = 0;
+  virtual void set_state(State state) = 0;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_INETWORKCONNECTION_H_
diff --git a/ntcore/src/main/native/cpp/IRpcServer.h b/ntcore/src/main/native/cpp/IRpcServer.h
new file mode 100644
index 0000000..dc8b0a6
--- /dev/null
+++ b/ntcore/src/main/native/cpp/IRpcServer.h
@@ -0,0 +1,38 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_IRPCSERVER_H_
+#define NTCORE_IRPCSERVER_H_
+
+#include <memory>
+
+#include "Message.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+class IRpcServer {
+ public:
+  typedef std::function<void(StringRef result)> SendResponseFunc;
+
+  IRpcServer() = default;
+  IRpcServer(const IRpcServer&) = delete;
+  IRpcServer& operator=(const IRpcServer&) = delete;
+  virtual ~IRpcServer() = default;
+
+  virtual void RemoveRpc(unsigned int rpc_uid) = 0;
+
+  virtual void ProcessRpc(unsigned int local_id, unsigned int call_uid,
+                          StringRef name, StringRef params,
+                          const ConnectionInfo& conn,
+                          SendResponseFunc send_response,
+                          unsigned int rpc_uid) = 0;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_IRPCSERVER_H_
diff --git a/ntcore/src/main/native/cpp/IStorage.h b/ntcore/src/main/native/cpp/IStorage.h
new file mode 100644
index 0000000..0fb3a0b
--- /dev/null
+++ b/ntcore/src/main/native/cpp/IStorage.h
@@ -0,0 +1,65 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_ISTORAGE_H_
+#define NTCORE_ISTORAGE_H_
+
+#include <functional>
+#include <memory>
+#include <vector>
+
+#include <wpi/ArrayRef.h>
+#include <wpi/Twine.h>
+
+#include "Message.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+class IDispatcher;
+class INetworkConnection;
+
+class IStorage {
+ public:
+  IStorage() = default;
+  IStorage(const IStorage&) = delete;
+  IStorage& operator=(const IStorage&) = delete;
+  virtual ~IStorage() = default;
+
+  // Accessors required by Dispatcher.  An interface is used for
+  // generation of outgoing messages to break a dependency loop between
+  // Storage and Dispatcher.
+  virtual void SetDispatcher(IDispatcher* dispatcher, bool server) = 0;
+  virtual void ClearDispatcher() = 0;
+
+  // Required for wire protocol 2.0 to get the entry type of an entry when
+  // receiving entry updates (because the length/type is not provided in the
+  // message itself).  Not used in wire protocol 3.0.
+  virtual NT_Type GetMessageEntryType(unsigned int id) const = 0;
+
+  virtual void ProcessIncoming(std::shared_ptr<Message> msg,
+                               INetworkConnection* conn,
+                               std::weak_ptr<INetworkConnection> conn_weak) = 0;
+  virtual void GetInitialAssignments(
+      INetworkConnection& conn,
+      std::vector<std::shared_ptr<Message>>* msgs) = 0;
+  virtual void ApplyInitialAssignments(
+      INetworkConnection& conn, wpi::ArrayRef<std::shared_ptr<Message>> msgs,
+      bool new_server, std::vector<std::shared_ptr<Message>>* out_msgs) = 0;
+
+  // Filename-based save/load functions.  Used both by periodic saves and
+  // accessible directly via the user API.
+  virtual const char* SavePersistent(const Twine& filename,
+                                     bool periodic) const = 0;
+  virtual const char* LoadPersistent(
+      const Twine& filename,
+      std::function<void(size_t line, const char* msg)> warn) = 0;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_ISTORAGE_H_
diff --git a/ntcore/src/main/native/cpp/InstanceImpl.cpp b/ntcore/src/main/native/cpp/InstanceImpl.cpp
new file mode 100644
index 0000000..3b34292
--- /dev/null
+++ b/ntcore/src/main/native/cpp/InstanceImpl.cpp
@@ -0,0 +1,109 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "InstanceImpl.h"
+
+using namespace nt;
+
+std::atomic<int> InstanceImpl::s_default{-1};
+std::atomic<InstanceImpl*> InstanceImpl::s_fast_instances[10];
+wpi::UidVector<InstanceImpl*, 10> InstanceImpl::s_instances;
+wpi::mutex InstanceImpl::s_mutex;
+
+using namespace std::placeholders;
+
+InstanceImpl::InstanceImpl(int inst)
+    : logger_impl(inst),
+      logger(std::bind(&LoggerImpl::Log, &logger_impl, _1, _2, _3, _4)),
+      connection_notifier(inst),
+      entry_notifier(inst, logger),
+      rpc_server(inst, logger),
+      storage(entry_notifier, rpc_server, logger),
+      dispatcher(storage, connection_notifier, logger),
+      ds_client(dispatcher, logger) {
+  logger.set_min_level(logger_impl.GetMinLevel());
+}
+
+InstanceImpl::~InstanceImpl() { logger.SetLogger(nullptr); }
+
+InstanceImpl* InstanceImpl::GetDefault() { return Get(GetDefaultIndex()); }
+
+InstanceImpl* InstanceImpl::Get(int inst) {
+  if (inst < 0) return nullptr;
+
+  // fast path, just an atomic read
+  if (static_cast<unsigned int>(inst) <
+      (sizeof(s_fast_instances) / sizeof(s_fast_instances[0]))) {
+    InstanceImpl* ptr = s_fast_instances[inst];
+    if (ptr) return ptr;
+  }
+
+  // slow path
+  std::lock_guard<wpi::mutex> lock(s_mutex);
+
+  // static fast-path block
+  if (static_cast<unsigned int>(inst) <
+      (sizeof(s_fast_instances) / sizeof(s_fast_instances[0]))) {
+    // double-check
+    return s_fast_instances[inst];
+  }
+
+  // vector
+  if (static_cast<unsigned int>(inst) < s_instances.size()) {
+    return s_instances[inst];
+  }
+
+  // doesn't exist
+  return nullptr;
+}
+
+int InstanceImpl::GetDefaultIndex() {
+  int inst = s_default;
+  if (inst >= 0) return inst;
+
+  // slow path
+  std::lock_guard<wpi::mutex> lock(s_mutex);
+
+  // double-check
+  inst = s_default;
+  if (inst >= 0) return inst;
+
+  // alloc and save
+  inst = AllocImpl();
+  s_default = inst;
+  return inst;
+}
+
+int InstanceImpl::Alloc() {
+  std::lock_guard<wpi::mutex> lock(s_mutex);
+  return AllocImpl();
+}
+
+int InstanceImpl::AllocImpl() {
+  unsigned int inst = s_instances.emplace_back(nullptr);
+  InstanceImpl* ptr = new InstanceImpl(inst);
+  s_instances[inst] = ptr;
+
+  if (inst < (sizeof(s_fast_instances) / sizeof(s_fast_instances[0]))) {
+    s_fast_instances[inst] = ptr;
+  }
+
+  return static_cast<int>(inst);
+}
+
+void InstanceImpl::Destroy(int inst) {
+  std::lock_guard<wpi::mutex> lock(s_mutex);
+  if (inst < 0 || static_cast<unsigned int>(inst) >= s_instances.size()) return;
+
+  if (static_cast<unsigned int>(inst) <
+      (sizeof(s_fast_instances) / sizeof(s_fast_instances[0]))) {
+    s_fast_instances[inst] = nullptr;
+  }
+
+  delete s_instances[inst];
+  s_instances.erase(inst);
+}
diff --git a/ntcore/src/main/native/cpp/InstanceImpl.h b/ntcore/src/main/native/cpp/InstanceImpl.h
new file mode 100644
index 0000000..6e732d8
--- /dev/null
+++ b/ntcore/src/main/native/cpp/InstanceImpl.h
@@ -0,0 +1,60 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_INSTANCEIMPL_H_
+#define NTCORE_INSTANCEIMPL_H_
+
+#include <atomic>
+#include <memory>
+
+#include <wpi/UidVector.h>
+#include <wpi/mutex.h>
+
+#include "ConnectionNotifier.h"
+#include "Dispatcher.h"
+#include "DsClient.h"
+#include "EntryNotifier.h"
+#include "Log.h"
+#include "LoggerImpl.h"
+#include "RpcServer.h"
+#include "Storage.h"
+
+namespace nt {
+
+class InstanceImpl {
+ public:
+  explicit InstanceImpl(int inst);
+  ~InstanceImpl();
+
+  // Instance repository
+  static InstanceImpl* GetDefault();
+  static InstanceImpl* Get(int inst);
+  static int GetDefaultIndex();
+  static int Alloc();
+  static void Destroy(int inst);
+
+  LoggerImpl logger_impl;
+  wpi::Logger logger;
+  ConnectionNotifier connection_notifier;
+  EntryNotifier entry_notifier;
+  RpcServer rpc_server;
+  Storage storage;
+  Dispatcher dispatcher;
+  DsClient ds_client;
+
+ private:
+  static int AllocImpl();
+
+  static std::atomic<int> s_default;
+  static std::atomic<InstanceImpl*> s_fast_instances[10];
+  static wpi::UidVector<InstanceImpl*, 10> s_instances;
+  static wpi::mutex s_mutex;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_INSTANCEIMPL_H_
diff --git a/ntcore/src/main/native/cpp/Log.h b/ntcore/src/main/native/cpp/Log.h
new file mode 100644
index 0000000..b8a3daf
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Log.h
@@ -0,0 +1,26 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_LOG_H_
+#define NTCORE_LOG_H_
+
+#include <wpi/Logger.h>
+
+#define LOG(level, x) WPI_LOG(m_logger, level, x)
+
+#undef ERROR
+#define ERROR(x) WPI_ERROR(m_logger, x)
+#define WARNING(x) WPI_WARNING(m_logger, x)
+#define INFO(x) WPI_INFO(m_logger, x)
+
+#define DEBUG(x) WPI_DEBUG(m_logger, x)
+#define DEBUG1(x) WPI_DEBUG1(m_logger, x)
+#define DEBUG2(x) WPI_DEBUG2(m_logger, x)
+#define DEBUG3(x) WPI_DEBUG3(m_logger, x)
+#define DEBUG4(x) WPI_DEBUG4(m_logger, x)
+
+#endif  // NTCORE_LOG_H_
diff --git a/ntcore/src/main/native/cpp/LoggerImpl.cpp b/ntcore/src/main/native/cpp/LoggerImpl.cpp
new file mode 100644
index 0000000..b2c6786
--- /dev/null
+++ b/ntcore/src/main/native/cpp/LoggerImpl.cpp
@@ -0,0 +1,77 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "LoggerImpl.h"
+
+#include <wpi/Path.h>
+#include <wpi/SmallString.h>
+#include <wpi/StringRef.h>
+#include <wpi/raw_ostream.h>
+
+using namespace nt;
+
+static void DefaultLogger(unsigned int level, const char* file,
+                          unsigned int line, const char* msg) {
+  wpi::SmallString<128> buf;
+  wpi::raw_svector_ostream oss(buf);
+  if (level == 20) {
+    oss << "NT: " << msg << '\n';
+    wpi::errs() << oss.str();
+    return;
+  }
+
+  wpi::StringRef levelmsg;
+  if (level >= 50)
+    levelmsg = "CRITICAL: ";
+  else if (level >= 40)
+    levelmsg = "ERROR: ";
+  else if (level >= 30)
+    levelmsg = "WARNING: ";
+  else
+    return;
+  oss << "NT: " << levelmsg << msg << " (" << file << ':' << line << ")\n";
+  wpi::errs() << oss.str();
+}
+
+LoggerImpl::LoggerImpl(int inst) : m_inst(inst) {}
+
+void LoggerImpl::Start() { DoStart(m_inst); }
+
+unsigned int LoggerImpl::Add(
+    std::function<void(const LogMessage& msg)> callback, unsigned int min_level,
+    unsigned int max_level) {
+  return DoAdd(callback, min_level, max_level);
+}
+
+unsigned int LoggerImpl::AddPolled(unsigned int poller_uid,
+                                   unsigned int min_level,
+                                   unsigned int max_level) {
+  return DoAdd(poller_uid, min_level, max_level);
+}
+
+unsigned int LoggerImpl::GetMinLevel() {
+  auto thr = GetThread();
+  if (!thr) return NT_LOG_INFO;
+  unsigned int level = NT_LOG_INFO;
+  for (size_t i = 0; i < thr->m_listeners.size(); ++i) {
+    const auto& listener = thr->m_listeners[i];
+    if (listener && listener.min_level < level) level = listener.min_level;
+  }
+  return level;
+}
+
+void LoggerImpl::Log(unsigned int level, const char* file, unsigned int line,
+                     const char* msg) {
+  // this is safe because it's null terminated and always the end
+  const char* filename = wpi::sys::path::filename(file).data();
+  {
+    auto thr = GetThread();
+    if (!thr || thr->m_listeners.empty())
+      DefaultLogger(level, filename, line, msg);
+  }
+  Send(UINT_MAX, 0, level, filename, line, msg);
+}
diff --git a/ntcore/src/main/native/cpp/LoggerImpl.h b/ntcore/src/main/native/cpp/LoggerImpl.h
new file mode 100644
index 0000000..3ac0295
--- /dev/null
+++ b/ntcore/src/main/native/cpp/LoggerImpl.h
@@ -0,0 +1,83 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_LOGGERIMPL_H_
+#define NTCORE_LOGGERIMPL_H_
+
+#include "CallbackManager.h"
+#include "Handle.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+namespace impl {
+
+struct LoggerListenerData
+    : public ListenerData<std::function<void(const LogMessage& msg)>> {
+  LoggerListenerData() = default;
+  LoggerListenerData(std::function<void(const LogMessage& msg)> callback_,
+                     unsigned int min_level_, unsigned int max_level_)
+      : ListenerData(callback_), min_level(min_level_), max_level(max_level_) {}
+  LoggerListenerData(unsigned int poller_uid_, unsigned int min_level_,
+                     unsigned int max_level_)
+      : ListenerData(poller_uid_),
+        min_level(min_level_),
+        max_level(max_level_) {}
+
+  unsigned int min_level;
+  unsigned int max_level;
+};
+
+class LoggerThread
+    : public CallbackThread<LoggerThread, LogMessage, LoggerListenerData> {
+ public:
+  explicit LoggerThread(int inst) : m_inst(inst) {}
+
+  bool Matches(const LoggerListenerData& listener, const LogMessage& data) {
+    return data.level >= listener.min_level && data.level <= listener.max_level;
+  }
+
+  void SetListener(LogMessage* data, unsigned int listener_uid) {
+    data->logger = Handle(m_inst, listener_uid, Handle::kLogger).handle();
+  }
+
+  void DoCallback(std::function<void(const LogMessage& msg)> callback,
+                  const LogMessage& data) {
+    callback(data);
+  }
+
+  int m_inst;
+};
+
+}  // namespace impl
+
+class LoggerImpl : public CallbackManager<LoggerImpl, impl::LoggerThread> {
+  friend class LoggerTest;
+  friend class CallbackManager<LoggerImpl, impl::LoggerThread>;
+
+ public:
+  explicit LoggerImpl(int inst);
+
+  void Start();
+
+  unsigned int Add(std::function<void(const LogMessage& msg)> callback,
+                   unsigned int min_level, unsigned int max_level);
+  unsigned int AddPolled(unsigned int poller_uid, unsigned int min_level,
+                         unsigned int max_level);
+
+  unsigned int GetMinLevel();
+
+  void Log(unsigned int level, const char* file, unsigned int line,
+           const char* msg);
+
+ private:
+  int m_inst;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_LOGGERIMPL_H_
diff --git a/ntcore/src/main/native/cpp/Message.cpp b/ntcore/src/main/native/cpp/Message.cpp
new file mode 100644
index 0000000..576e444
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Message.cpp
@@ -0,0 +1,301 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "Message.h"
+
+#include <stdint.h>
+
+#include "Log.h"
+#include "WireDecoder.h"
+#include "WireEncoder.h"
+
+#define kClearAllMagic 0xD06CB27Aul
+
+using namespace nt;
+
+std::shared_ptr<Message> Message::Read(WireDecoder& decoder,
+                                       GetEntryTypeFunc get_entry_type) {
+  unsigned int msg_type = 0;
+  if (!decoder.Read8(&msg_type)) return nullptr;
+  auto msg =
+      std::make_shared<Message>(static_cast<MsgType>(msg_type), private_init());
+  switch (msg_type) {
+    case kKeepAlive:
+      break;
+    case kClientHello: {
+      unsigned int proto_rev;
+      if (!decoder.Read16(&proto_rev)) return nullptr;
+      msg->m_id = proto_rev;
+      // This intentionally uses the provided proto_rev instead of
+      // decoder.proto_rev().
+      if (proto_rev >= 0x0300u) {
+        if (!decoder.ReadString(&msg->m_str)) return nullptr;
+      }
+      break;
+    }
+    case kProtoUnsup: {
+      if (!decoder.Read16(&msg->m_id)) return nullptr;  // proto rev
+      break;
+    }
+    case kServerHelloDone:
+      break;
+    case kServerHello:
+      if (decoder.proto_rev() < 0x0300u) {
+        decoder.set_error("received SERVER_HELLO in protocol < 3.0");
+        return nullptr;
+      }
+      if (!decoder.Read8(&msg->m_flags)) return nullptr;
+      if (!decoder.ReadString(&msg->m_str)) return nullptr;
+      break;
+    case kClientHelloDone:
+      if (decoder.proto_rev() < 0x0300u) {
+        decoder.set_error("received CLIENT_HELLO_DONE in protocol < 3.0");
+        return nullptr;
+      }
+      break;
+    case kEntryAssign: {
+      if (!decoder.ReadString(&msg->m_str)) return nullptr;  // name
+      NT_Type type;
+      if (!decoder.ReadType(&type)) return nullptr;              // entry type
+      if (!decoder.Read16(&msg->m_id)) return nullptr;           // id
+      if (!decoder.Read16(&msg->m_seq_num_uid)) return nullptr;  // seq num
+      if (decoder.proto_rev() >= 0x0300u) {
+        if (!decoder.Read8(&msg->m_flags)) return nullptr;  // flags
+      }
+      msg->m_value = decoder.ReadValue(type);
+      if (!msg->m_value) return nullptr;
+      break;
+    }
+    case kEntryUpdate: {
+      if (!decoder.Read16(&msg->m_id)) return nullptr;           // id
+      if (!decoder.Read16(&msg->m_seq_num_uid)) return nullptr;  // seq num
+      NT_Type type;
+      if (decoder.proto_rev() >= 0x0300u) {
+        if (!decoder.ReadType(&type)) return nullptr;
+      } else {
+        type = get_entry_type(msg->m_id);
+      }
+      WPI_DEBUG4(decoder.logger(), "update message data type: " << type);
+      msg->m_value = decoder.ReadValue(type);
+      if (!msg->m_value) return nullptr;
+      break;
+    }
+    case kFlagsUpdate: {
+      if (decoder.proto_rev() < 0x0300u) {
+        decoder.set_error("received FLAGS_UPDATE in protocol < 3.0");
+        return nullptr;
+      }
+      if (!decoder.Read16(&msg->m_id)) return nullptr;
+      if (!decoder.Read8(&msg->m_flags)) return nullptr;
+      break;
+    }
+    case kEntryDelete: {
+      if (decoder.proto_rev() < 0x0300u) {
+        decoder.set_error("received ENTRY_DELETE in protocol < 3.0");
+        return nullptr;
+      }
+      if (!decoder.Read16(&msg->m_id)) return nullptr;
+      break;
+    }
+    case kClearEntries: {
+      if (decoder.proto_rev() < 0x0300u) {
+        decoder.set_error("received CLEAR_ENTRIES in protocol < 3.0");
+        return nullptr;
+      }
+      uint32_t magic;
+      if (!decoder.Read32(&magic)) return nullptr;
+      if (magic != kClearAllMagic) {
+        decoder.set_error(
+            "received incorrect CLEAR_ENTRIES magic value, ignoring");
+        return nullptr;
+      }
+      break;
+    }
+    case kExecuteRpc: {
+      if (decoder.proto_rev() < 0x0300u) {
+        decoder.set_error("received EXECUTE_RPC in protocol < 3.0");
+        return nullptr;
+      }
+      if (!decoder.Read16(&msg->m_id)) return nullptr;
+      if (!decoder.Read16(&msg->m_seq_num_uid)) return nullptr;  // uid
+      uint64_t size;
+      if (!decoder.ReadUleb128(&size)) return nullptr;
+      const char* params;
+      if (!decoder.Read(&params, size)) return nullptr;
+      msg->m_str = wpi::StringRef(params, size);
+      break;
+    }
+    case kRpcResponse: {
+      if (decoder.proto_rev() < 0x0300u) {
+        decoder.set_error("received RPC_RESPONSE in protocol < 3.0");
+        return nullptr;
+      }
+      if (!decoder.Read16(&msg->m_id)) return nullptr;
+      if (!decoder.Read16(&msg->m_seq_num_uid)) return nullptr;  // uid
+      uint64_t size;
+      if (!decoder.ReadUleb128(&size)) return nullptr;
+      const char* results;
+      if (!decoder.Read(&results, size)) return nullptr;
+      msg->m_str = wpi::StringRef(results, size);
+      break;
+    }
+    default:
+      decoder.set_error("unrecognized message type");
+      WPI_INFO(decoder.logger(), "unrecognized message type: " << msg_type);
+      return nullptr;
+  }
+  return msg;
+}
+
+std::shared_ptr<Message> Message::ClientHello(wpi::StringRef self_id) {
+  auto msg = std::make_shared<Message>(kClientHello, private_init());
+  msg->m_str = self_id;
+  return msg;
+}
+
+std::shared_ptr<Message> Message::ServerHello(unsigned int flags,
+                                              wpi::StringRef self_id) {
+  auto msg = std::make_shared<Message>(kServerHello, private_init());
+  msg->m_str = self_id;
+  msg->m_flags = flags;
+  return msg;
+}
+
+std::shared_ptr<Message> Message::EntryAssign(wpi::StringRef name,
+                                              unsigned int id,
+                                              unsigned int seq_num,
+                                              std::shared_ptr<Value> value,
+                                              unsigned int flags) {
+  auto msg = std::make_shared<Message>(kEntryAssign, private_init());
+  msg->m_str = name;
+  msg->m_value = value;
+  msg->m_id = id;
+  msg->m_flags = flags;
+  msg->m_seq_num_uid = seq_num;
+  return msg;
+}
+
+std::shared_ptr<Message> Message::EntryUpdate(unsigned int id,
+                                              unsigned int seq_num,
+                                              std::shared_ptr<Value> value) {
+  auto msg = std::make_shared<Message>(kEntryUpdate, private_init());
+  msg->m_value = value;
+  msg->m_id = id;
+  msg->m_seq_num_uid = seq_num;
+  return msg;
+}
+
+std::shared_ptr<Message> Message::FlagsUpdate(unsigned int id,
+                                              unsigned int flags) {
+  auto msg = std::make_shared<Message>(kFlagsUpdate, private_init());
+  msg->m_id = id;
+  msg->m_flags = flags;
+  return msg;
+}
+
+std::shared_ptr<Message> Message::EntryDelete(unsigned int id) {
+  auto msg = std::make_shared<Message>(kEntryDelete, private_init());
+  msg->m_id = id;
+  return msg;
+}
+
+std::shared_ptr<Message> Message::ExecuteRpc(unsigned int id, unsigned int uid,
+                                             wpi::StringRef params) {
+  auto msg = std::make_shared<Message>(kExecuteRpc, private_init());
+  msg->m_str = params;
+  msg->m_id = id;
+  msg->m_seq_num_uid = uid;
+  return msg;
+}
+
+std::shared_ptr<Message> Message::RpcResponse(unsigned int id, unsigned int uid,
+                                              wpi::StringRef result) {
+  auto msg = std::make_shared<Message>(kRpcResponse, private_init());
+  msg->m_str = result;
+  msg->m_id = id;
+  msg->m_seq_num_uid = uid;
+  return msg;
+}
+
+void Message::Write(WireEncoder& encoder) const {
+  switch (m_type) {
+    case kKeepAlive:
+      encoder.Write8(kKeepAlive);
+      break;
+    case kClientHello:
+      encoder.Write8(kClientHello);
+      encoder.Write16(encoder.proto_rev());
+      if (encoder.proto_rev() < 0x0300u) return;
+      encoder.WriteString(m_str);
+      break;
+    case kProtoUnsup:
+      encoder.Write8(kProtoUnsup);
+      encoder.Write16(encoder.proto_rev());
+      break;
+    case kServerHelloDone:
+      encoder.Write8(kServerHelloDone);
+      break;
+    case kServerHello:
+      if (encoder.proto_rev() < 0x0300u) return;  // new message in version 3.0
+      encoder.Write8(kServerHello);
+      encoder.Write8(m_flags);
+      encoder.WriteString(m_str);
+      break;
+    case kClientHelloDone:
+      if (encoder.proto_rev() < 0x0300u) return;  // new message in version 3.0
+      encoder.Write8(kClientHelloDone);
+      break;
+    case kEntryAssign:
+      encoder.Write8(kEntryAssign);
+      encoder.WriteString(m_str);
+      encoder.WriteType(m_value->type());
+      encoder.Write16(m_id);
+      encoder.Write16(m_seq_num_uid);
+      if (encoder.proto_rev() >= 0x0300u) encoder.Write8(m_flags);
+      encoder.WriteValue(*m_value);
+      break;
+    case kEntryUpdate:
+      encoder.Write8(kEntryUpdate);
+      encoder.Write16(m_id);
+      encoder.Write16(m_seq_num_uid);
+      if (encoder.proto_rev() >= 0x0300u) encoder.WriteType(m_value->type());
+      encoder.WriteValue(*m_value);
+      break;
+    case kFlagsUpdate:
+      if (encoder.proto_rev() < 0x0300u) return;  // new message in version 3.0
+      encoder.Write8(kFlagsUpdate);
+      encoder.Write16(m_id);
+      encoder.Write8(m_flags);
+      break;
+    case kEntryDelete:
+      if (encoder.proto_rev() < 0x0300u) return;  // new message in version 3.0
+      encoder.Write8(kEntryDelete);
+      encoder.Write16(m_id);
+      break;
+    case kClearEntries:
+      if (encoder.proto_rev() < 0x0300u) return;  // new message in version 3.0
+      encoder.Write8(kClearEntries);
+      encoder.Write32(kClearAllMagic);
+      break;
+    case kExecuteRpc:
+      if (encoder.proto_rev() < 0x0300u) return;  // new message in version 3.0
+      encoder.Write8(kExecuteRpc);
+      encoder.Write16(m_id);
+      encoder.Write16(m_seq_num_uid);
+      encoder.WriteString(m_str);
+      break;
+    case kRpcResponse:
+      if (encoder.proto_rev() < 0x0300u) return;  // new message in version 3.0
+      encoder.Write8(kRpcResponse);
+      encoder.Write16(m_id);
+      encoder.Write16(m_seq_num_uid);
+      encoder.WriteString(m_str);
+      break;
+    default:
+      break;
+  }
+}
diff --git a/ntcore/src/main/native/cpp/Message.h b/ntcore/src/main/native/cpp/Message.h
new file mode 100644
index 0000000..9b25abf
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Message.h
@@ -0,0 +1,117 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_MESSAGE_H_
+#define NTCORE_MESSAGE_H_
+
+#include <functional>
+#include <memory>
+#include <string>
+
+#include "networktables/NetworkTableValue.h"
+
+namespace nt {
+
+class WireDecoder;
+class WireEncoder;
+
+class Message {
+  struct private_init {};
+
+ public:
+  enum MsgType {
+    kUnknown = -1,
+    kKeepAlive = 0x00,
+    kClientHello = 0x01,
+    kProtoUnsup = 0x02,
+    kServerHelloDone = 0x03,
+    kServerHello = 0x04,
+    kClientHelloDone = 0x05,
+    kEntryAssign = 0x10,
+    kEntryUpdate = 0x11,
+    kFlagsUpdate = 0x12,
+    kEntryDelete = 0x13,
+    kClearEntries = 0x14,
+    kExecuteRpc = 0x20,
+    kRpcResponse = 0x21
+  };
+  typedef std::function<NT_Type(unsigned int id)> GetEntryTypeFunc;
+
+  Message() : m_type(kUnknown), m_id(0), m_flags(0), m_seq_num_uid(0) {}
+  Message(MsgType type, const private_init&)
+      : m_type(type), m_id(0), m_flags(0), m_seq_num_uid(0) {}
+
+  MsgType type() const { return m_type; }
+  bool Is(MsgType type) const { return type == m_type; }
+
+  // Message data accessors.  Callers are responsible for knowing what data is
+  // actually provided for a particular message.
+  wpi::StringRef str() const { return m_str; }
+  std::shared_ptr<Value> value() const { return m_value; }
+  unsigned int id() const { return m_id; }
+  unsigned int flags() const { return m_flags; }
+  unsigned int seq_num_uid() const { return m_seq_num_uid; }
+
+  // Read and write from wire representation
+  void Write(WireEncoder& encoder) const;
+  static std::shared_ptr<Message> Read(WireDecoder& decoder,
+                                       GetEntryTypeFunc get_entry_type);
+
+  // Create messages without data
+  static std::shared_ptr<Message> KeepAlive() {
+    return std::make_shared<Message>(kKeepAlive, private_init());
+  }
+  static std::shared_ptr<Message> ProtoUnsup() {
+    return std::make_shared<Message>(kProtoUnsup, private_init());
+  }
+  static std::shared_ptr<Message> ServerHelloDone() {
+    return std::make_shared<Message>(kServerHelloDone, private_init());
+  }
+  static std::shared_ptr<Message> ClientHelloDone() {
+    return std::make_shared<Message>(kClientHelloDone, private_init());
+  }
+  static std::shared_ptr<Message> ClearEntries() {
+    return std::make_shared<Message>(kClearEntries, private_init());
+  }
+
+  // Create messages with data
+  static std::shared_ptr<Message> ClientHello(wpi::StringRef self_id);
+  static std::shared_ptr<Message> ServerHello(unsigned int flags,
+                                              wpi::StringRef self_id);
+  static std::shared_ptr<Message> EntryAssign(wpi::StringRef name,
+                                              unsigned int id,
+                                              unsigned int seq_num,
+                                              std::shared_ptr<Value> value,
+                                              unsigned int flags);
+  static std::shared_ptr<Message> EntryUpdate(unsigned int id,
+                                              unsigned int seq_num,
+                                              std::shared_ptr<Value> value);
+  static std::shared_ptr<Message> FlagsUpdate(unsigned int id,
+                                              unsigned int flags);
+  static std::shared_ptr<Message> EntryDelete(unsigned int id);
+  static std::shared_ptr<Message> ExecuteRpc(unsigned int id, unsigned int uid,
+                                             wpi::StringRef params);
+  static std::shared_ptr<Message> RpcResponse(unsigned int id, unsigned int uid,
+                                              wpi::StringRef result);
+
+  Message(const Message&) = delete;
+  Message& operator=(const Message&) = delete;
+
+ private:
+  MsgType m_type;
+
+  // Message data.  Use varies by message type.
+  std::string m_str;
+  std::shared_ptr<Value> m_value;
+  unsigned int m_id;  // also used for proto_rev
+  unsigned int m_flags;
+  unsigned int m_seq_num_uid;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_MESSAGE_H_
diff --git a/ntcore/src/main/native/cpp/NetworkConnection.cpp b/ntcore/src/main/native/cpp/NetworkConnection.cpp
new file mode 100644
index 0000000..cb1e333
--- /dev/null
+++ b/ntcore/src/main/native/cpp/NetworkConnection.cpp
@@ -0,0 +1,333 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "NetworkConnection.h"
+
+#include <wpi/NetworkStream.h>
+#include <wpi/raw_socket_istream.h>
+#include <wpi/timestamp.h>
+
+#include "IConnectionNotifier.h"
+#include "Log.h"
+#include "WireDecoder.h"
+#include "WireEncoder.h"
+
+using namespace nt;
+
+NetworkConnection::NetworkConnection(unsigned int uid,
+                                     std::unique_ptr<wpi::NetworkStream> stream,
+                                     IConnectionNotifier& notifier,
+                                     wpi::Logger& logger,
+                                     HandshakeFunc handshake,
+                                     Message::GetEntryTypeFunc get_entry_type)
+    : m_uid(uid),
+      m_stream(std::move(stream)),
+      m_notifier(notifier),
+      m_logger(logger),
+      m_handshake(handshake),
+      m_get_entry_type(get_entry_type),
+      m_state(kCreated) {
+  m_active = false;
+  m_proto_rev = 0x0300;
+  m_last_update = 0;
+
+  // turn off Nagle algorithm; we bundle packets for transmission
+  m_stream->setNoDelay();
+}
+
+NetworkConnection::~NetworkConnection() { Stop(); }
+
+void NetworkConnection::Start() {
+  if (m_active) return;
+  m_active = true;
+  set_state(kInit);
+  // clear queue
+  while (!m_outgoing.empty()) m_outgoing.pop();
+  // reset shutdown flags
+  {
+    std::lock_guard<wpi::mutex> lock(m_shutdown_mutex);
+    m_read_shutdown = false;
+    m_write_shutdown = false;
+  }
+  // start threads
+  m_write_thread = std::thread(&NetworkConnection::WriteThreadMain, this);
+  m_read_thread = std::thread(&NetworkConnection::ReadThreadMain, this);
+}
+
+void NetworkConnection::Stop() {
+  DEBUG2("NetworkConnection stopping (" << this << ")");
+  set_state(kDead);
+  m_active = false;
+  // closing the stream so the read thread terminates
+  if (m_stream) m_stream->close();
+  // send an empty outgoing message set so the write thread terminates
+  m_outgoing.push(Outgoing());
+  // wait for threads to terminate, with timeout
+  if (m_write_thread.joinable()) {
+    std::unique_lock<wpi::mutex> lock(m_shutdown_mutex);
+    auto timeout_time =
+        std::chrono::steady_clock::now() + std::chrono::milliseconds(200);
+    if (m_write_shutdown_cv.wait_until(lock, timeout_time,
+                                       [&] { return m_write_shutdown; }))
+      m_write_thread.join();
+    else
+      m_write_thread.detach();  // timed out, detach it
+  }
+  if (m_read_thread.joinable()) {
+    std::unique_lock<wpi::mutex> lock(m_shutdown_mutex);
+    auto timeout_time =
+        std::chrono::steady_clock::now() + std::chrono::milliseconds(200);
+    if (m_read_shutdown_cv.wait_until(lock, timeout_time,
+                                      [&] { return m_read_shutdown; }))
+      m_read_thread.join();
+    else
+      m_read_thread.detach();  // timed out, detach it
+  }
+  // clear queue
+  while (!m_outgoing.empty()) m_outgoing.pop();
+}
+
+ConnectionInfo NetworkConnection::info() const {
+  return ConnectionInfo{remote_id(), m_stream->getPeerIP(),
+                        static_cast<unsigned int>(m_stream->getPeerPort()),
+                        m_last_update, m_proto_rev};
+}
+
+unsigned int NetworkConnection::proto_rev() const { return m_proto_rev; }
+
+void NetworkConnection::set_proto_rev(unsigned int proto_rev) {
+  m_proto_rev = proto_rev;
+}
+
+NetworkConnection::State NetworkConnection::state() const {
+  std::lock_guard<wpi::mutex> lock(m_state_mutex);
+  return m_state;
+}
+
+void NetworkConnection::set_state(State state) {
+  std::lock_guard<wpi::mutex> lock(m_state_mutex);
+  // Don't update state any more once we've died
+  if (m_state == kDead) return;
+  // One-shot notify state changes
+  if (m_state != kActive && state == kActive)
+    m_notifier.NotifyConnection(true, info());
+  if (m_state != kDead && state == kDead)
+    m_notifier.NotifyConnection(false, info());
+  m_state = state;
+}
+
+std::string NetworkConnection::remote_id() const {
+  std::lock_guard<wpi::mutex> lock(m_remote_id_mutex);
+  return m_remote_id;
+}
+
+void NetworkConnection::set_remote_id(StringRef remote_id) {
+  std::lock_guard<wpi::mutex> lock(m_remote_id_mutex);
+  m_remote_id = remote_id;
+}
+
+void NetworkConnection::ReadThreadMain() {
+  wpi::raw_socket_istream is(*m_stream);
+  WireDecoder decoder(is, m_proto_rev, m_logger);
+
+  set_state(kHandshake);
+  if (!m_handshake(*this,
+                   [&] {
+                     decoder.set_proto_rev(m_proto_rev);
+                     auto msg = Message::Read(decoder, m_get_entry_type);
+                     if (!msg && decoder.error())
+                       DEBUG("error reading in handshake: " << decoder.error());
+                     return msg;
+                   },
+                   [&](wpi::ArrayRef<std::shared_ptr<Message>> msgs) {
+                     m_outgoing.emplace(msgs);
+                   })) {
+    set_state(kDead);
+    m_active = false;
+    goto done;
+  }
+
+  set_state(kActive);
+  while (m_active) {
+    if (!m_stream) break;
+    decoder.set_proto_rev(m_proto_rev);
+    decoder.Reset();
+    auto msg = Message::Read(decoder, m_get_entry_type);
+    if (!msg) {
+      if (decoder.error()) INFO("read error: " << decoder.error());
+      // terminate connection on bad message
+      if (m_stream) m_stream->close();
+      break;
+    }
+    DEBUG3("received type=" << msg->type() << " with str=" << msg->str()
+                            << " id=" << msg->id()
+                            << " seq_num=" << msg->seq_num_uid());
+    m_last_update = Now();
+    m_process_incoming(std::move(msg), this);
+  }
+  DEBUG2("read thread died (" << this << ")");
+  set_state(kDead);
+  m_active = false;
+  m_outgoing.push(Outgoing());  // also kill write thread
+
+done:
+  // use condition variable to signal thread shutdown
+  {
+    std::lock_guard<wpi::mutex> lock(m_shutdown_mutex);
+    m_read_shutdown = true;
+    m_read_shutdown_cv.notify_one();
+  }
+}
+
+void NetworkConnection::WriteThreadMain() {
+  WireEncoder encoder(m_proto_rev);
+
+  while (m_active) {
+    auto msgs = m_outgoing.pop();
+    DEBUG4("write thread woke up");
+    if (msgs.empty()) continue;
+    encoder.set_proto_rev(m_proto_rev);
+    encoder.Reset();
+    DEBUG3("sending " << msgs.size() << " messages");
+    for (auto& msg : msgs) {
+      if (msg) {
+        DEBUG3("sending type=" << msg->type() << " with str=" << msg->str()
+                               << " id=" << msg->id()
+                               << " seq_num=" << msg->seq_num_uid());
+        msg->Write(encoder);
+      }
+    }
+    wpi::NetworkStream::Error err;
+    if (!m_stream) break;
+    if (encoder.size() == 0) continue;
+    if (m_stream->send(encoder.data(), encoder.size(), &err) == 0) break;
+    DEBUG4("sent " << encoder.size() << " bytes");
+  }
+  DEBUG2("write thread died (" << this << ")");
+  set_state(kDead);
+  m_active = false;
+  if (m_stream) m_stream->close();  // also kill read thread
+
+  // use condition variable to signal thread shutdown
+  {
+    std::lock_guard<wpi::mutex> lock(m_shutdown_mutex);
+    m_write_shutdown = true;
+    m_write_shutdown_cv.notify_one();
+  }
+}
+
+void NetworkConnection::QueueOutgoing(std::shared_ptr<Message> msg) {
+  std::lock_guard<wpi::mutex> lock(m_pending_mutex);
+
+  // Merge with previous.  One case we don't combine: delete/assign loop.
+  switch (msg->type()) {
+    case Message::kEntryAssign:
+    case Message::kEntryUpdate: {
+      // don't do this for unassigned id's
+      unsigned int id = msg->id();
+      if (id == 0xffff) {
+        m_pending_outgoing.push_back(msg);
+        break;
+      }
+      if (id < m_pending_update.size() && m_pending_update[id].first != 0) {
+        // overwrite the previous one for this id
+        auto& oldmsg = m_pending_outgoing[m_pending_update[id].first - 1];
+        if (oldmsg && oldmsg->Is(Message::kEntryAssign) &&
+            msg->Is(Message::kEntryUpdate)) {
+          // need to update assignment with new seq_num and value
+          oldmsg = Message::EntryAssign(oldmsg->str(), id, msg->seq_num_uid(),
+                                        msg->value(), oldmsg->flags());
+        } else {
+          oldmsg = msg;  // easy update
+        }
+      } else {
+        // new, but remember it
+        size_t pos = m_pending_outgoing.size();
+        m_pending_outgoing.push_back(msg);
+        if (id >= m_pending_update.size()) m_pending_update.resize(id + 1);
+        m_pending_update[id].first = pos + 1;
+      }
+      break;
+    }
+    case Message::kEntryDelete: {
+      // don't do this for unassigned id's
+      unsigned int id = msg->id();
+      if (id == 0xffff) {
+        m_pending_outgoing.push_back(msg);
+        break;
+      }
+
+      // clear previous updates
+      if (id < m_pending_update.size()) {
+        if (m_pending_update[id].first != 0) {
+          m_pending_outgoing[m_pending_update[id].first - 1].reset();
+          m_pending_update[id].first = 0;
+        }
+        if (m_pending_update[id].second != 0) {
+          m_pending_outgoing[m_pending_update[id].second - 1].reset();
+          m_pending_update[id].second = 0;
+        }
+      }
+
+      // add deletion
+      m_pending_outgoing.push_back(msg);
+      break;
+    }
+    case Message::kFlagsUpdate: {
+      // don't do this for unassigned id's
+      unsigned int id = msg->id();
+      if (id == 0xffff) {
+        m_pending_outgoing.push_back(msg);
+        break;
+      }
+      if (id < m_pending_update.size() && m_pending_update[id].second != 0) {
+        // overwrite the previous one for this id
+        m_pending_outgoing[m_pending_update[id].second - 1] = msg;
+      } else {
+        // new, but remember it
+        size_t pos = m_pending_outgoing.size();
+        m_pending_outgoing.push_back(msg);
+        if (id >= m_pending_update.size()) m_pending_update.resize(id + 1);
+        m_pending_update[id].second = pos + 1;
+      }
+      break;
+    }
+    case Message::kClearEntries: {
+      // knock out all previous assigns/updates!
+      for (auto& i : m_pending_outgoing) {
+        if (!i) continue;
+        auto t = i->type();
+        if (t == Message::kEntryAssign || t == Message::kEntryUpdate ||
+            t == Message::kFlagsUpdate || t == Message::kEntryDelete ||
+            t == Message::kClearEntries)
+          i.reset();
+      }
+      m_pending_update.resize(0);
+      m_pending_outgoing.push_back(msg);
+      break;
+    }
+    default:
+      m_pending_outgoing.push_back(msg);
+      break;
+  }
+}
+
+void NetworkConnection::PostOutgoing(bool keep_alive) {
+  std::lock_guard<wpi::mutex> lock(m_pending_mutex);
+  auto now = std::chrono::steady_clock::now();
+  if (m_pending_outgoing.empty()) {
+    if (!keep_alive) return;
+    // send keep-alives once a second (if no other messages have been sent)
+    if ((now - m_last_post) < std::chrono::seconds(1)) return;
+    m_outgoing.emplace(Outgoing{Message::KeepAlive()});
+  } else {
+    m_outgoing.emplace(std::move(m_pending_outgoing));
+    m_pending_outgoing.resize(0);
+    m_pending_update.resize(0);
+  }
+  m_last_post = now;
+}
diff --git a/ntcore/src/main/native/cpp/NetworkConnection.h b/ntcore/src/main/native/cpp/NetworkConnection.h
new file mode 100644
index 0000000..91ad64e
--- /dev/null
+++ b/ntcore/src/main/native/cpp/NetworkConnection.h
@@ -0,0 +1,127 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NETWORKCONNECTION_H_
+#define NTCORE_NETWORKCONNECTION_H_
+
+#include <stdint.h>
+
+#include <atomic>
+#include <chrono>
+#include <memory>
+#include <string>
+#include <thread>
+#include <utility>
+#include <vector>
+
+#include <wpi/ConcurrentQueue.h>
+#include <wpi/condition_variable.h>
+#include <wpi/mutex.h>
+
+#include "INetworkConnection.h"
+#include "Message.h"
+#include "ntcore_cpp.h"
+
+namespace wpi {
+class Logger;
+class NetworkStream;
+}  // namespace wpi
+
+namespace nt {
+
+class IConnectionNotifier;
+
+class NetworkConnection : public INetworkConnection {
+ public:
+  typedef std::function<bool(
+      NetworkConnection& conn,
+      std::function<std::shared_ptr<Message>()> get_msg,
+      std::function<void(wpi::ArrayRef<std::shared_ptr<Message>>)> send_msgs)>
+      HandshakeFunc;
+  typedef std::function<void(std::shared_ptr<Message> msg,
+                             NetworkConnection* conn)>
+      ProcessIncomingFunc;
+  typedef std::vector<std::shared_ptr<Message>> Outgoing;
+  typedef wpi::ConcurrentQueue<Outgoing> OutgoingQueue;
+
+  NetworkConnection(unsigned int uid,
+                    std::unique_ptr<wpi::NetworkStream> stream,
+                    IConnectionNotifier& notifier, wpi::Logger& logger,
+                    HandshakeFunc handshake,
+                    Message::GetEntryTypeFunc get_entry_type);
+  ~NetworkConnection();
+
+  // Set the input processor function.  This must be called before Start().
+  void set_process_incoming(ProcessIncomingFunc func) {
+    m_process_incoming = func;
+  }
+
+  void Start();
+  void Stop();
+
+  ConnectionInfo info() const override;
+
+  bool active() const { return m_active; }
+  wpi::NetworkStream& stream() { return *m_stream; }
+
+  void QueueOutgoing(std::shared_ptr<Message> msg) override;
+  void PostOutgoing(bool keep_alive) override;
+
+  unsigned int uid() const { return m_uid; }
+
+  unsigned int proto_rev() const override;
+  void set_proto_rev(unsigned int proto_rev) override;
+
+  State state() const override;
+  void set_state(State state) override;
+
+  std::string remote_id() const;
+  void set_remote_id(StringRef remote_id);
+
+  uint64_t last_update() const { return m_last_update; }
+
+  NetworkConnection(const NetworkConnection&) = delete;
+  NetworkConnection& operator=(const NetworkConnection&) = delete;
+
+ private:
+  void ReadThreadMain();
+  void WriteThreadMain();
+
+  unsigned int m_uid;
+  std::unique_ptr<wpi::NetworkStream> m_stream;
+  IConnectionNotifier& m_notifier;
+  wpi::Logger& m_logger;
+  OutgoingQueue m_outgoing;
+  HandshakeFunc m_handshake;
+  Message::GetEntryTypeFunc m_get_entry_type;
+  ProcessIncomingFunc m_process_incoming;
+  std::thread m_read_thread;
+  std::thread m_write_thread;
+  std::atomic_bool m_active;
+  std::atomic_uint m_proto_rev;
+  mutable wpi::mutex m_state_mutex;
+  State m_state;
+  mutable wpi::mutex m_remote_id_mutex;
+  std::string m_remote_id;
+  std::atomic_ullong m_last_update;
+  std::chrono::steady_clock::time_point m_last_post;
+
+  wpi::mutex m_pending_mutex;
+  Outgoing m_pending_outgoing;
+  std::vector<std::pair<size_t, size_t>> m_pending_update;
+
+  // Condition variables for shutdown
+  wpi::mutex m_shutdown_mutex;
+  wpi::condition_variable m_read_shutdown_cv;
+  wpi::condition_variable m_write_shutdown_cv;
+  bool m_read_shutdown = false;
+  bool m_write_shutdown = false;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_NETWORKCONNECTION_H_
diff --git a/ntcore/src/main/native/cpp/RpcServer.cpp b/ntcore/src/main/native/cpp/RpcServer.cpp
new file mode 100644
index 0000000..0c6811c
--- /dev/null
+++ b/ntcore/src/main/native/cpp/RpcServer.cpp
@@ -0,0 +1,49 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "RpcServer.h"
+
+using namespace nt;
+
+RpcServer::RpcServer(int inst, wpi::Logger& logger)
+    : m_inst(inst), m_logger(logger) {}
+
+void RpcServer::Start() { DoStart(m_inst, m_logger); }
+
+unsigned int RpcServer::Add(
+    std::function<void(const RpcAnswer& answer)> callback) {
+  return DoAdd(callback);
+}
+
+unsigned int RpcServer::AddPolled(unsigned int poller_uid) {
+  return DoAdd(poller_uid);
+}
+
+void RpcServer::RemoveRpc(unsigned int rpc_uid) { Remove(rpc_uid); }
+
+void RpcServer::ProcessRpc(unsigned int local_id, unsigned int call_uid,
+                           StringRef name, StringRef params,
+                           const ConnectionInfo& conn,
+                           SendResponseFunc send_response,
+                           unsigned int rpc_uid) {
+  Send(rpc_uid, Handle(m_inst, local_id, Handle::kEntry).handle(),
+       Handle(m_inst, call_uid, Handle::kRpcCall).handle(), name, params, conn,
+       send_response);
+}
+
+bool RpcServer::PostRpcResponse(unsigned int local_id, unsigned int call_uid,
+                                wpi::StringRef result) {
+  auto thr = GetThread();
+  auto i = thr->m_response_map.find(impl::RpcIdPair{local_id, call_uid});
+  if (i == thr->m_response_map.end()) {
+    WARNING("posting RPC response to nonexistent call (or duplicate response)");
+    return false;
+  }
+  (i->getSecond())(result);
+  thr->m_response_map.erase(i);
+  return true;
+}
diff --git a/ntcore/src/main/native/cpp/RpcServer.h b/ntcore/src/main/native/cpp/RpcServer.h
new file mode 100644
index 0000000..5afe62a
--- /dev/null
+++ b/ntcore/src/main/native/cpp/RpcServer.h
@@ -0,0 +1,113 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_RPCSERVER_H_
+#define NTCORE_RPCSERVER_H_
+
+#include <utility>
+
+#include <wpi/DenseMap.h>
+#include <wpi/mutex.h>
+
+#include "CallbackManager.h"
+#include "Handle.h"
+#include "IRpcServer.h"
+#include "Log.h"
+
+namespace nt {
+
+namespace impl {
+
+typedef std::pair<unsigned int, unsigned int> RpcIdPair;
+
+struct RpcNotifierData : public RpcAnswer {
+  RpcNotifierData(NT_Entry entry_, NT_RpcCall call_, StringRef name_,
+                  StringRef params_, const ConnectionInfo& conn_,
+                  IRpcServer::SendResponseFunc send_response_)
+      : RpcAnswer{entry_, call_, name_, params_, conn_},
+        send_response{send_response_} {}
+
+  IRpcServer::SendResponseFunc send_response;
+};
+
+using RpcListenerData =
+    ListenerData<std::function<void(const RpcAnswer& answer)>>;
+
+class RpcServerThread
+    : public CallbackThread<RpcServerThread, RpcAnswer, RpcListenerData,
+                            RpcNotifierData> {
+ public:
+  RpcServerThread(int inst, wpi::Logger& logger)
+      : m_inst(inst), m_logger(logger) {}
+
+  bool Matches(const RpcListenerData& /*listener*/,
+               const RpcNotifierData& data) {
+    return !data.name.empty() && data.send_response;
+  }
+
+  void SetListener(RpcNotifierData* data, unsigned int /*listener_uid*/) {
+    unsigned int local_id = Handle{data->entry}.GetIndex();
+    unsigned int call_uid = Handle{data->call}.GetIndex();
+    RpcIdPair lookup_uid{local_id, call_uid};
+    m_response_map.insert(std::make_pair(lookup_uid, data->send_response));
+  }
+
+  void DoCallback(std::function<void(const RpcAnswer& call)> callback,
+                  const RpcNotifierData& data) {
+    DEBUG4("rpc calling " << data.name);
+    unsigned int local_id = Handle{data.entry}.GetIndex();
+    unsigned int call_uid = Handle{data.call}.GetIndex();
+    RpcIdPair lookup_uid{local_id, call_uid};
+    callback(data);
+    {
+      std::lock_guard<wpi::mutex> lock(m_mutex);
+      auto i = m_response_map.find(lookup_uid);
+      if (i != m_response_map.end()) {
+        // post an empty response and erase it
+        (i->getSecond())("");
+        m_response_map.erase(i);
+      }
+    }
+  }
+
+  int m_inst;
+  wpi::Logger& m_logger;
+  wpi::DenseMap<RpcIdPair, IRpcServer::SendResponseFunc> m_response_map;
+};
+
+}  // namespace impl
+
+class RpcServer : public IRpcServer,
+                  public CallbackManager<RpcServer, impl::RpcServerThread> {
+  friend class RpcServerTest;
+  friend class CallbackManager<RpcServer, impl::RpcServerThread>;
+
+ public:
+  RpcServer(int inst, wpi::Logger& logger);
+
+  void Start();
+
+  unsigned int Add(std::function<void(const RpcAnswer& answer)> callback);
+  unsigned int AddPolled(unsigned int poller_uid);
+  void RemoveRpc(unsigned int rpc_uid) override;
+
+  void ProcessRpc(unsigned int local_id, unsigned int call_uid, StringRef name,
+                  StringRef params, const ConnectionInfo& conn,
+                  SendResponseFunc send_response,
+                  unsigned int rpc_uid) override;
+
+  bool PostRpcResponse(unsigned int local_id, unsigned int call_uid,
+                       wpi::StringRef result);
+
+ private:
+  int m_inst;
+  wpi::Logger& m_logger;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_RPCSERVER_H_
diff --git a/ntcore/src/main/native/cpp/SequenceNumber.cpp b/ntcore/src/main/native/cpp/SequenceNumber.cpp
new file mode 100644
index 0000000..6d61331
--- /dev/null
+++ b/ntcore/src/main/native/cpp/SequenceNumber.cpp
@@ -0,0 +1,30 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "SequenceNumber.h"
+
+namespace nt {
+
+bool operator<(const SequenceNumber& lhs, const SequenceNumber& rhs) {
+  if (lhs.m_value < rhs.m_value)
+    return (rhs.m_value - lhs.m_value) < (1u << 15);
+  else if (lhs.m_value > rhs.m_value)
+    return (lhs.m_value - rhs.m_value) > (1u << 15);
+  else
+    return false;
+}
+
+bool operator>(const SequenceNumber& lhs, const SequenceNumber& rhs) {
+  if (lhs.m_value < rhs.m_value)
+    return (rhs.m_value - lhs.m_value) > (1u << 15);
+  else if (lhs.m_value > rhs.m_value)
+    return (lhs.m_value - rhs.m_value) < (1u << 15);
+  else
+    return false;
+}
+
+}  // namespace nt
diff --git a/ntcore/src/main/native/cpp/SequenceNumber.h b/ntcore/src/main/native/cpp/SequenceNumber.h
new file mode 100644
index 0000000..11d9953
--- /dev/null
+++ b/ntcore/src/main/native/cpp/SequenceNumber.h
@@ -0,0 +1,63 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_SEQUENCENUMBER_H_
+#define NTCORE_SEQUENCENUMBER_H_
+
+namespace nt {
+
+/* A sequence number per RFC 1982 */
+class SequenceNumber {
+ public:
+  SequenceNumber() : m_value(0) {}
+  explicit SequenceNumber(unsigned int value) : m_value(value) {}
+  unsigned int value() const { return m_value; }
+
+  SequenceNumber& operator++() {
+    ++m_value;
+    if (m_value > 0xffff) m_value = 0;
+    return *this;
+  }
+  SequenceNumber operator++(int) {
+    SequenceNumber tmp(*this);
+    operator++();
+    return tmp;
+  }
+
+  friend bool operator<(const SequenceNumber& lhs, const SequenceNumber& rhs);
+  friend bool operator>(const SequenceNumber& lhs, const SequenceNumber& rhs);
+  friend bool operator<=(const SequenceNumber& lhs, const SequenceNumber& rhs);
+  friend bool operator>=(const SequenceNumber& lhs, const SequenceNumber& rhs);
+  friend bool operator==(const SequenceNumber& lhs, const SequenceNumber& rhs);
+  friend bool operator!=(const SequenceNumber& lhs, const SequenceNumber& rhs);
+
+ private:
+  unsigned int m_value;
+};
+
+bool operator<(const SequenceNumber& lhs, const SequenceNumber& rhs);
+bool operator>(const SequenceNumber& lhs, const SequenceNumber& rhs);
+
+inline bool operator<=(const SequenceNumber& lhs, const SequenceNumber& rhs) {
+  return lhs == rhs || lhs < rhs;
+}
+
+inline bool operator>=(const SequenceNumber& lhs, const SequenceNumber& rhs) {
+  return lhs == rhs || lhs > rhs;
+}
+
+inline bool operator==(const SequenceNumber& lhs, const SequenceNumber& rhs) {
+  return lhs.m_value == rhs.m_value;
+}
+
+inline bool operator!=(const SequenceNumber& lhs, const SequenceNumber& rhs) {
+  return lhs.m_value != rhs.m_value;
+}
+
+}  // namespace nt
+
+#endif  // NTCORE_SEQUENCENUMBER_H_
diff --git a/ntcore/src/main/native/cpp/Storage.cpp b/ntcore/src/main/native/cpp/Storage.cpp
new file mode 100644
index 0000000..0d8d467
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Storage.cpp
@@ -0,0 +1,1131 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "Storage.h"
+
+#include <wpi/timestamp.h>
+
+#include "Handle.h"
+#include "IDispatcher.h"
+#include "IEntryNotifier.h"
+#include "INetworkConnection.h"
+#include "IRpcServer.h"
+#include "Log.h"
+
+using namespace nt;
+
+Storage::Storage(IEntryNotifier& notifier, IRpcServer& rpc_server,
+                 wpi::Logger& logger)
+    : m_notifier(notifier), m_rpc_server(rpc_server), m_logger(logger) {
+  m_terminating = false;
+}
+
+Storage::~Storage() {
+  m_terminating = true;
+  m_rpc_results_cond.notify_all();
+}
+
+void Storage::SetDispatcher(IDispatcher* dispatcher, bool server) {
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  m_dispatcher = dispatcher;
+  m_server = server;
+}
+
+void Storage::ClearDispatcher() { m_dispatcher = nullptr; }
+
+NT_Type Storage::GetMessageEntryType(unsigned int id) const {
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  if (id >= m_idmap.size()) return NT_UNASSIGNED;
+  Entry* entry = m_idmap[id];
+  if (!entry || !entry->value) return NT_UNASSIGNED;
+  return entry->value->type();
+}
+
+void Storage::ProcessIncoming(std::shared_ptr<Message> msg,
+                              INetworkConnection* conn,
+                              std::weak_ptr<INetworkConnection> conn_weak) {
+  switch (msg->type()) {
+    case Message::kKeepAlive:
+      break;  // ignore
+    case Message::kClientHello:
+    case Message::kProtoUnsup:
+    case Message::kServerHelloDone:
+    case Message::kServerHello:
+    case Message::kClientHelloDone:
+      // shouldn't get these, but ignore if we do
+      break;
+    case Message::kEntryAssign:
+      ProcessIncomingEntryAssign(std::move(msg), conn);
+      break;
+    case Message::kEntryUpdate:
+      ProcessIncomingEntryUpdate(std::move(msg), conn);
+      break;
+    case Message::kFlagsUpdate:
+      ProcessIncomingFlagsUpdate(std::move(msg), conn);
+      break;
+    case Message::kEntryDelete:
+      ProcessIncomingEntryDelete(std::move(msg), conn);
+      break;
+    case Message::kClearEntries:
+      ProcessIncomingClearEntries(std::move(msg), conn);
+      break;
+    case Message::kExecuteRpc:
+      ProcessIncomingExecuteRpc(std::move(msg), conn, std::move(conn_weak));
+      break;
+    case Message::kRpcResponse:
+      ProcessIncomingRpcResponse(std::move(msg), conn);
+      break;
+    default:
+      break;
+  }
+}
+
+void Storage::ProcessIncomingEntryAssign(std::shared_ptr<Message> msg,
+                                         INetworkConnection* conn) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  unsigned int id = msg->id();
+  StringRef name = msg->str();
+  Entry* entry;
+  bool may_need_update = false;
+  SequenceNumber seq_num(msg->seq_num_uid());
+  if (m_server) {
+    // if we're a server, id=0xffff requests are requests for an id
+    // to be assigned, and we need to send the new assignment back to
+    // the sender as well as all other connections.
+    if (id == 0xffff) {
+      entry = GetOrNew(name);
+      // see if it was already assigned; ignore if so.
+      if (entry->id != 0xffff) return;
+
+      entry->flags = msg->flags();
+      entry->seq_num = seq_num;
+      SetEntryValueImpl(entry, msg->value(), lock, false);
+      return;
+    }
+    if (id >= m_idmap.size() || !m_idmap[id]) {
+      // ignore arbitrary entry assignments
+      // this can happen due to e.g. assignment to deleted entry
+      lock.unlock();
+      DEBUG("server: received assignment to unknown entry");
+      return;
+    }
+    entry = m_idmap[id];
+  } else {
+    // clients simply accept new assignments
+    if (id == 0xffff) {
+      lock.unlock();
+      DEBUG("client: received entry assignment request?");
+      return;
+    }
+    if (id >= m_idmap.size()) m_idmap.resize(id + 1);
+    entry = m_idmap[id];
+    if (!entry) {
+      // create local
+      entry = GetOrNew(name);
+      entry->id = id;
+      m_idmap[id] = entry;
+      if (!entry->value) {
+        // didn't exist at all (rather than just being a response to a
+        // id assignment request)
+        entry->value = msg->value();
+        entry->flags = msg->flags();
+        entry->seq_num = seq_num;
+
+        // notify
+        m_notifier.NotifyEntry(entry->local_id, name, entry->value,
+                               NT_NOTIFY_NEW);
+        return;
+      }
+      may_need_update = true;  // we may need to send an update message
+
+      // if the received flags don't match what we sent, we most likely
+      // updated flags locally in the interim; send flags update message.
+      if (msg->flags() != entry->flags) {
+        auto dispatcher = m_dispatcher;
+        auto outmsg = Message::FlagsUpdate(id, entry->flags);
+        lock.unlock();
+        dispatcher->QueueOutgoing(outmsg, nullptr, nullptr);
+        lock.lock();
+      }
+    }
+  }
+
+  // common client and server handling
+
+  // already exists; ignore if sequence number not higher than local
+  if (seq_num < entry->seq_num) {
+    if (may_need_update) {
+      auto dispatcher = m_dispatcher;
+      auto outmsg =
+          Message::EntryUpdate(entry->id, entry->seq_num.value(), entry->value);
+      lock.unlock();
+      dispatcher->QueueOutgoing(outmsg, nullptr, nullptr);
+    }
+    return;
+  }
+
+  // sanity check: name should match id
+  if (msg->str() != entry->name) {
+    lock.unlock();
+    DEBUG("entry assignment for same id with different name?");
+    return;
+  }
+
+  unsigned int notify_flags = NT_NOTIFY_UPDATE;
+
+  // don't update flags from a <3.0 remote (not part of message)
+  // don't update flags if this is a server response to a client id request
+  if (!may_need_update && conn->proto_rev() >= 0x0300) {
+    // update persistent dirty flag if persistent flag changed
+    if ((entry->flags & NT_PERSISTENT) != (msg->flags() & NT_PERSISTENT))
+      m_persistent_dirty = true;
+    if (entry->flags != msg->flags()) notify_flags |= NT_NOTIFY_FLAGS;
+    entry->flags = msg->flags();
+  }
+
+  // update persistent dirty flag if the value changed and it's persistent
+  if (entry->IsPersistent() && *entry->value != *msg->value())
+    m_persistent_dirty = true;
+
+  // update local
+  entry->value = msg->value();
+  entry->seq_num = seq_num;
+
+  // notify
+  m_notifier.NotifyEntry(entry->local_id, name, entry->value, notify_flags);
+
+  // broadcast to all other connections (note for client there won't
+  // be any other connections, so don't bother)
+  if (m_server && m_dispatcher) {
+    auto dispatcher = m_dispatcher;
+    auto outmsg = Message::EntryAssign(entry->name, id, msg->seq_num_uid(),
+                                       msg->value(), entry->flags);
+    lock.unlock();
+    dispatcher->QueueOutgoing(outmsg, nullptr, conn);
+  }
+}
+
+void Storage::ProcessIncomingEntryUpdate(std::shared_ptr<Message> msg,
+                                         INetworkConnection* conn) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  unsigned int id = msg->id();
+  if (id >= m_idmap.size() || !m_idmap[id]) {
+    // ignore arbitrary entry updates;
+    // this can happen due to deleted entries
+    lock.unlock();
+    DEBUG("received update to unknown entry");
+    return;
+  }
+  Entry* entry = m_idmap[id];
+
+  // ignore if sequence number not higher than local
+  SequenceNumber seq_num(msg->seq_num_uid());
+  if (seq_num <= entry->seq_num) return;
+
+  // update local
+  entry->value = msg->value();
+  entry->seq_num = seq_num;
+
+  // update persistent dirty flag if it's a persistent value
+  if (entry->IsPersistent()) m_persistent_dirty = true;
+
+  // notify
+  m_notifier.NotifyEntry(entry->local_id, entry->name, entry->value,
+                         NT_NOTIFY_UPDATE);
+
+  // broadcast to all other connections (note for client there won't
+  // be any other connections, so don't bother)
+  if (m_server && m_dispatcher) {
+    auto dispatcher = m_dispatcher;
+    lock.unlock();
+    dispatcher->QueueOutgoing(msg, nullptr, conn);
+  }
+}
+
+void Storage::ProcessIncomingFlagsUpdate(std::shared_ptr<Message> msg,
+                                         INetworkConnection* conn) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  unsigned int id = msg->id();
+  if (id >= m_idmap.size() || !m_idmap[id]) {
+    // ignore arbitrary entry updates;
+    // this can happen due to deleted entries
+    lock.unlock();
+    DEBUG("received flags update to unknown entry");
+    return;
+  }
+
+  // update local
+  SetEntryFlagsImpl(m_idmap[id], msg->flags(), lock, false);
+
+  // broadcast to all other connections (note for client there won't
+  // be any other connections, so don't bother)
+  if (m_server && m_dispatcher) {
+    auto dispatcher = m_dispatcher;
+    lock.unlock();
+    dispatcher->QueueOutgoing(msg, nullptr, conn);
+  }
+}
+
+void Storage::ProcessIncomingEntryDelete(std::shared_ptr<Message> msg,
+                                         INetworkConnection* conn) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  unsigned int id = msg->id();
+  if (id >= m_idmap.size() || !m_idmap[id]) {
+    // ignore arbitrary entry updates;
+    // this can happen due to deleted entries
+    lock.unlock();
+    DEBUG("received delete to unknown entry");
+    return;
+  }
+
+  // update local
+  DeleteEntryImpl(m_idmap[id], lock, false);
+
+  // broadcast to all other connections (note for client there won't
+  // be any other connections, so don't bother)
+  if (m_server && m_dispatcher) {
+    auto dispatcher = m_dispatcher;
+    lock.unlock();
+    dispatcher->QueueOutgoing(msg, nullptr, conn);
+  }
+}
+
+void Storage::ProcessIncomingClearEntries(std::shared_ptr<Message> msg,
+                                          INetworkConnection* conn) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  // update local
+  DeleteAllEntriesImpl(false);
+
+  // broadcast to all other connections (note for client there won't
+  // be any other connections, so don't bother)
+  if (m_server && m_dispatcher) {
+    auto dispatcher = m_dispatcher;
+    lock.unlock();
+    dispatcher->QueueOutgoing(msg, nullptr, conn);
+  }
+}
+
+void Storage::ProcessIncomingExecuteRpc(
+    std::shared_ptr<Message> msg, INetworkConnection* /*conn*/,
+    std::weak_ptr<INetworkConnection> conn_weak) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (!m_server) return;  // only process on server
+  unsigned int id = msg->id();
+  if (id >= m_idmap.size() || !m_idmap[id]) {
+    // ignore call to non-existent RPC
+    // this can happen due to deleted entries
+    lock.unlock();
+    DEBUG("received RPC call to unknown entry");
+    return;
+  }
+  Entry* entry = m_idmap[id];
+  if (!entry->value || !entry->value->IsRpc()) {
+    lock.unlock();
+    DEBUG("received RPC call to non-RPC entry");
+    return;
+  }
+  ConnectionInfo conn_info;
+  auto c = conn_weak.lock();
+  if (c) {
+    conn_info = c->info();
+  } else {
+    conn_info.remote_id = "";
+    conn_info.remote_ip = "";
+    conn_info.remote_port = 0;
+    conn_info.last_update = 0;
+    conn_info.protocol_version = 0;
+  }
+  unsigned int call_uid = msg->seq_num_uid();
+  m_rpc_server.ProcessRpc(
+      entry->local_id, call_uid, entry->name, msg->str(), conn_info,
+      [=](StringRef result) {
+        auto c = conn_weak.lock();
+        if (c) c->QueueOutgoing(Message::RpcResponse(id, call_uid, result));
+      },
+      entry->rpc_uid);
+}
+
+void Storage::ProcessIncomingRpcResponse(std::shared_ptr<Message> msg,
+                                         INetworkConnection* /*conn*/) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (m_server) return;  // only process on client
+  unsigned int id = msg->id();
+  if (id >= m_idmap.size() || !m_idmap[id]) {
+    // ignore response to non-existent RPC
+    // this can happen due to deleted entries
+    lock.unlock();
+    DEBUG("received rpc response to unknown entry");
+    return;
+  }
+  Entry* entry = m_idmap[id];
+  if (!entry->value || !entry->value->IsRpc()) {
+    lock.unlock();
+    DEBUG("received RPC response to non-RPC entry");
+    return;
+  }
+  m_rpc_results.insert(std::make_pair(
+      RpcIdPair{entry->local_id, msg->seq_num_uid()}, msg->str()));
+  m_rpc_results_cond.notify_all();
+}
+
+void Storage::GetInitialAssignments(
+    INetworkConnection& conn, std::vector<std::shared_ptr<Message>>* msgs) {
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  conn.set_state(INetworkConnection::kSynchronized);
+  for (auto& i : m_entries) {
+    Entry* entry = i.getValue();
+    if (!entry->value) continue;
+    msgs->emplace_back(Message::EntryAssign(i.getKey(), entry->id,
+                                            entry->seq_num.value(),
+                                            entry->value, entry->flags));
+  }
+}
+
+void Storage::ApplyInitialAssignments(
+    INetworkConnection& conn, wpi::ArrayRef<std::shared_ptr<Message>> msgs,
+    bool /*new_server*/, std::vector<std::shared_ptr<Message>>* out_msgs) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (m_server) return;  // should not do this on server
+
+  conn.set_state(INetworkConnection::kSynchronized);
+
+  std::vector<std::shared_ptr<Message>> update_msgs;
+
+  // clear existing id's
+  for (auto& i : m_entries) i.getValue()->id = 0xffff;
+
+  // clear existing idmap
+  m_idmap.resize(0);
+
+  // apply assignments
+  for (auto& msg : msgs) {
+    if (!msg->Is(Message::kEntryAssign)) {
+      DEBUG("client: received non-entry assignment request?");
+      continue;
+    }
+
+    unsigned int id = msg->id();
+    if (id == 0xffff) {
+      DEBUG("client: received entry assignment request?");
+      continue;
+    }
+
+    SequenceNumber seq_num(msg->seq_num_uid());
+    StringRef name = msg->str();
+
+    Entry* entry = GetOrNew(name);
+    entry->seq_num = seq_num;
+    entry->id = id;
+    if (!entry->value) {
+      // doesn't currently exist
+      entry->value = msg->value();
+      entry->flags = msg->flags();
+      // notify
+      m_notifier.NotifyEntry(entry->local_id, name, entry->value,
+                             NT_NOTIFY_NEW);
+    } else {
+      // if we have written the value locally and the value is not persistent,
+      // then we don't update the local value and instead send it back to the
+      // server as an update message
+      if (entry->local_write && !entry->IsPersistent()) {
+        ++entry->seq_num;
+        update_msgs.emplace_back(Message::EntryUpdate(
+            entry->id, entry->seq_num.value(), entry->value));
+      } else {
+        entry->value = msg->value();
+        unsigned int notify_flags = NT_NOTIFY_UPDATE;
+        // don't update flags from a <3.0 remote (not part of message)
+        if (conn.proto_rev() >= 0x0300) {
+          if (entry->flags != msg->flags()) notify_flags |= NT_NOTIFY_FLAGS;
+          entry->flags = msg->flags();
+        }
+        // notify
+        m_notifier.NotifyEntry(entry->local_id, name, entry->value,
+                               notify_flags);
+      }
+    }
+
+    // save to idmap
+    if (id >= m_idmap.size()) m_idmap.resize(id + 1);
+    m_idmap[id] = entry;
+  }
+
+  // delete or generate assign messages for unassigned local entries
+  DeleteAllEntriesImpl(false, [&](Entry* entry) -> bool {
+    // was assigned by the server, don't delete
+    if (entry->id != 0xffff) return false;
+    // if we have written the value locally, we send an assign message to the
+    // server instead of deleting
+    if (entry->local_write) {
+      out_msgs->emplace_back(Message::EntryAssign(entry->name, entry->id,
+                                                  entry->seq_num.value(),
+                                                  entry->value, entry->flags));
+      return false;
+    }
+    // otherwise delete
+    return true;
+  });
+  auto dispatcher = m_dispatcher;
+  lock.unlock();
+  for (auto& msg : update_msgs)
+    dispatcher->QueueOutgoing(msg, nullptr, nullptr);
+}
+
+std::shared_ptr<Value> Storage::GetEntryValue(StringRef name) const {
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  auto i = m_entries.find(name);
+  if (i == m_entries.end()) return nullptr;
+  return i->getValue()->value;
+}
+
+std::shared_ptr<Value> Storage::GetEntryValue(unsigned int local_id) const {
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  if (local_id >= m_localmap.size()) return nullptr;
+  return m_localmap[local_id]->value;
+}
+
+bool Storage::SetDefaultEntryValue(StringRef name,
+                                   std::shared_ptr<Value> value) {
+  if (name.empty()) return false;
+  if (!value) return false;
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  Entry* entry = GetOrNew(name);
+
+  // we return early if value already exists; if types match return true
+  if (entry->value) return entry->value->type() == value->type();
+
+  SetEntryValueImpl(entry, value, lock, true);
+  return true;
+}
+
+bool Storage::SetDefaultEntryValue(unsigned int local_id,
+                                   std::shared_ptr<Value> value) {
+  if (!value) return false;
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (local_id >= m_localmap.size()) return false;
+  Entry* entry = m_localmap[local_id].get();
+
+  // we return early if value already exists; if types match return true
+  if (entry->value) return entry->value->type() == value->type();
+
+  SetEntryValueImpl(entry, value, lock, true);
+  return true;
+}
+
+bool Storage::SetEntryValue(StringRef name, std::shared_ptr<Value> value) {
+  if (name.empty()) return true;
+  if (!value) return true;
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  Entry* entry = GetOrNew(name);
+
+  if (entry->value && entry->value->type() != value->type())
+    return false;  // error on type mismatch
+
+  SetEntryValueImpl(entry, value, lock, true);
+  return true;
+}
+
+bool Storage::SetEntryValue(unsigned int local_id,
+                            std::shared_ptr<Value> value) {
+  if (!value) return true;
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (local_id >= m_localmap.size()) return true;
+  Entry* entry = m_localmap[local_id].get();
+
+  if (entry->value && entry->value->type() != value->type())
+    return false;  // error on type mismatch
+
+  SetEntryValueImpl(entry, value, lock, true);
+  return true;
+}
+
+void Storage::SetEntryValueImpl(Entry* entry, std::shared_ptr<Value> value,
+                                std::unique_lock<wpi::mutex>& lock,
+                                bool local) {
+  if (!value) return;
+  auto old_value = entry->value;
+  entry->value = value;
+
+  // if we're the server, assign an id if it doesn't have one
+  if (m_server && entry->id == 0xffff) {
+    unsigned int id = m_idmap.size();
+    entry->id = id;
+    m_idmap.push_back(entry);
+  }
+
+  // update persistent dirty flag if value changed and it's persistent
+  if (entry->IsPersistent() && (!old_value || *old_value != *value))
+    m_persistent_dirty = true;
+
+  // notify
+  if (!old_value)
+    m_notifier.NotifyEntry(entry->local_id, entry->name, value,
+                           NT_NOTIFY_NEW | (local ? NT_NOTIFY_LOCAL : 0));
+  else if (*old_value != *value)
+    m_notifier.NotifyEntry(entry->local_id, entry->name, value,
+                           NT_NOTIFY_UPDATE | (local ? NT_NOTIFY_LOCAL : 0));
+
+  // remember local changes
+  if (local) entry->local_write = true;
+
+  // generate message
+  if (!m_dispatcher || (!local && !m_server)) return;
+  auto dispatcher = m_dispatcher;
+  if (!old_value || old_value->type() != value->type()) {
+    if (local) ++entry->seq_num;
+    auto msg = Message::EntryAssign(
+        entry->name, entry->id, entry->seq_num.value(), value, entry->flags);
+    lock.unlock();
+    dispatcher->QueueOutgoing(msg, nullptr, nullptr);
+  } else if (*old_value != *value) {
+    if (local) ++entry->seq_num;
+    // don't send an update if we don't have an assigned id yet
+    if (entry->id != 0xffff) {
+      auto msg = Message::EntryUpdate(entry->id, entry->seq_num.value(), value);
+      lock.unlock();
+      dispatcher->QueueOutgoing(msg, nullptr, nullptr);
+    }
+  }
+}
+
+void Storage::SetEntryTypeValue(StringRef name, std::shared_ptr<Value> value) {
+  if (name.empty()) return;
+  if (!value) return;
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  Entry* entry = GetOrNew(name);
+
+  SetEntryValueImpl(entry, value, lock, true);
+}
+
+void Storage::SetEntryTypeValue(unsigned int local_id,
+                                std::shared_ptr<Value> value) {
+  if (!value) return;
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (local_id >= m_localmap.size()) return;
+  Entry* entry = m_localmap[local_id].get();
+  if (!entry) return;
+
+  SetEntryValueImpl(entry, value, lock, true);
+}
+
+void Storage::SetEntryFlags(StringRef name, unsigned int flags) {
+  if (name.empty()) return;
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  auto i = m_entries.find(name);
+  if (i == m_entries.end()) return;
+  SetEntryFlagsImpl(i->getValue(), flags, lock, true);
+}
+
+void Storage::SetEntryFlags(unsigned int id_local, unsigned int flags) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (id_local >= m_localmap.size()) return;
+  SetEntryFlagsImpl(m_localmap[id_local].get(), flags, lock, true);
+}
+
+void Storage::SetEntryFlagsImpl(Entry* entry, unsigned int flags,
+                                std::unique_lock<wpi::mutex>& lock,
+                                bool local) {
+  if (!entry->value || entry->flags == flags) return;
+
+  // update persistent dirty flag if persistent flag changed
+  if ((entry->flags & NT_PERSISTENT) != (flags & NT_PERSISTENT))
+    m_persistent_dirty = true;
+
+  entry->flags = flags;
+
+  // notify
+  m_notifier.NotifyEntry(entry->local_id, entry->name, entry->value,
+                         NT_NOTIFY_FLAGS | (local ? NT_NOTIFY_LOCAL : 0));
+
+  // generate message
+  if (!local || !m_dispatcher) return;
+  auto dispatcher = m_dispatcher;
+  unsigned int id = entry->id;
+  // don't send an update if we don't have an assigned id yet
+  if (id != 0xffff) {
+    lock.unlock();
+    dispatcher->QueueOutgoing(Message::FlagsUpdate(id, flags), nullptr,
+                              nullptr);
+  }
+}
+
+unsigned int Storage::GetEntryFlags(StringRef name) const {
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  auto i = m_entries.find(name);
+  if (i == m_entries.end()) return 0;
+  return i->getValue()->flags;
+}
+
+unsigned int Storage::GetEntryFlags(unsigned int local_id) const {
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  if (local_id >= m_localmap.size()) return 0;
+  return m_localmap[local_id]->flags;
+}
+
+void Storage::DeleteEntry(StringRef name) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  auto i = m_entries.find(name);
+  if (i == m_entries.end()) return;
+  DeleteEntryImpl(i->getValue(), lock, true);
+}
+
+void Storage::DeleteEntry(unsigned int local_id) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (local_id >= m_localmap.size()) return;
+  DeleteEntryImpl(m_localmap[local_id].get(), lock, true);
+}
+
+void Storage::DeleteEntryImpl(Entry* entry, std::unique_lock<wpi::mutex>& lock,
+                              bool local) {
+  unsigned int id = entry->id;
+
+  // Erase entry from id mapping.
+  if (id < m_idmap.size()) m_idmap[id] = nullptr;
+
+  // empty the value and reset id and local_write flag
+  std::shared_ptr<Value> old_value;
+  old_value.swap(entry->value);
+  entry->id = 0xffff;
+  entry->local_write = false;
+
+  // remove RPC if there was one
+  if (entry->rpc_uid != UINT_MAX) {
+    m_rpc_server.RemoveRpc(entry->rpc_uid);
+    entry->rpc_uid = UINT_MAX;
+  }
+
+  // update persistent dirty flag if it's a persistent value
+  if (entry->IsPersistent()) m_persistent_dirty = true;
+
+  // reset flags
+  entry->flags = 0;
+
+  if (!old_value) return;  // was not previously assigned
+
+  // notify
+  m_notifier.NotifyEntry(entry->local_id, entry->name, old_value,
+                         NT_NOTIFY_DELETE | (local ? NT_NOTIFY_LOCAL : 0));
+
+  // if it had a value, generate message
+  // don't send an update if we don't have an assigned id yet
+  if (local && id != 0xffff) {
+    if (!m_dispatcher) return;
+    auto dispatcher = m_dispatcher;
+    lock.unlock();
+    dispatcher->QueueOutgoing(Message::EntryDelete(id), nullptr, nullptr);
+  }
+}
+
+template <typename F>
+void Storage::DeleteAllEntriesImpl(bool local, F should_delete) {
+  for (auto& i : m_entries) {
+    Entry* entry = i.getValue();
+    if (entry->value && should_delete(entry)) {
+      // notify it's being deleted
+      m_notifier.NotifyEntry(entry->local_id, i.getKey(), entry->value,
+                             NT_NOTIFY_DELETE | (local ? NT_NOTIFY_LOCAL : 0));
+      // remove it from idmap
+      if (entry->id < m_idmap.size()) m_idmap[entry->id] = nullptr;
+      entry->id = 0xffff;
+      entry->local_write = false;
+      entry->value.reset();
+      continue;
+    }
+  }
+}
+
+void Storage::DeleteAllEntriesImpl(bool local) {
+  // only delete non-persistent values
+  DeleteAllEntriesImpl(local,
+                       [](Entry* entry) { return !entry->IsPersistent(); });
+}
+
+void Storage::DeleteAllEntries() {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (m_entries.empty()) return;
+
+  DeleteAllEntriesImpl(true);
+
+  // generate message
+  if (!m_dispatcher) return;
+  auto dispatcher = m_dispatcher;
+  lock.unlock();
+  dispatcher->QueueOutgoing(Message::ClearEntries(), nullptr, nullptr);
+}
+
+Storage::Entry* Storage::GetOrNew(const Twine& name) {
+  wpi::SmallString<128> nameBuf;
+  StringRef nameStr = name.toStringRef(nameBuf);
+  auto& entry = m_entries[nameStr];
+  if (!entry) {
+    m_localmap.emplace_back(new Entry(nameStr));
+    entry = m_localmap.back().get();
+    entry->local_id = m_localmap.size() - 1;
+  }
+  return entry;
+}
+
+unsigned int Storage::GetEntry(const Twine& name) {
+  if (name.isTriviallyEmpty() ||
+      (name.isSingleStringRef() && name.getSingleStringRef().empty()))
+    return UINT_MAX;
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  return GetOrNew(name)->local_id;
+}
+
+std::vector<unsigned int> Storage::GetEntries(const Twine& prefix,
+                                              unsigned int types) {
+  wpi::SmallString<128> prefixBuf;
+  StringRef prefixStr = prefix.toStringRef(prefixBuf);
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  std::vector<unsigned int> ids;
+  for (auto& i : m_entries) {
+    Entry* entry = i.getValue();
+    auto value = entry->value.get();
+    if (!value || !i.getKey().startswith(prefixStr)) continue;
+    if (types != 0 && (types & value->type()) == 0) continue;
+    ids.push_back(entry->local_id);
+  }
+  return ids;
+}
+
+EntryInfo Storage::GetEntryInfo(int inst, unsigned int local_id) const {
+  EntryInfo info;
+  info.entry = 0;
+  info.type = NT_UNASSIGNED;
+  info.flags = 0;
+  info.last_change = 0;
+
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (local_id >= m_localmap.size()) return info;
+  Entry* entry = m_localmap[local_id].get();
+  if (!entry->value) return info;
+
+  info.entry = Handle(inst, local_id, Handle::kEntry);
+  info.name = entry->name;
+  info.type = entry->value->type();
+  info.flags = entry->flags;
+  info.last_change = entry->value->last_change();
+  return info;
+}
+
+std::string Storage::GetEntryName(unsigned int local_id) const {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (local_id >= m_localmap.size()) return std::string{};
+  return m_localmap[local_id]->name;
+}
+
+NT_Type Storage::GetEntryType(unsigned int local_id) const {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (local_id >= m_localmap.size()) return NT_UNASSIGNED;
+  Entry* entry = m_localmap[local_id].get();
+  if (!entry->value) return NT_UNASSIGNED;
+  return entry->value->type();
+}
+
+uint64_t Storage::GetEntryLastChange(unsigned int local_id) const {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (local_id >= m_localmap.size()) return 0;
+  Entry* entry = m_localmap[local_id].get();
+  if (!entry->value) return 0;
+  return entry->value->last_change();
+}
+
+std::vector<EntryInfo> Storage::GetEntryInfo(int inst, const Twine& prefix,
+                                             unsigned int types) {
+  wpi::SmallString<128> prefixBuf;
+  StringRef prefixStr = prefix.toStringRef(prefixBuf);
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  std::vector<EntryInfo> infos;
+  for (auto& i : m_entries) {
+    Entry* entry = i.getValue();
+    auto value = entry->value.get();
+    if (!value || !i.getKey().startswith(prefixStr)) continue;
+    if (types != 0 && (types & value->type()) == 0) continue;
+    EntryInfo info;
+    info.entry = Handle(inst, entry->local_id, Handle::kEntry);
+    info.name = i.getKey();
+    info.type = value->type();
+    info.flags = entry->flags;
+    info.last_change = value->last_change();
+    infos.push_back(std::move(info));
+  }
+  return infos;
+}
+
+unsigned int Storage::AddListener(
+    const Twine& prefix,
+    std::function<void(const EntryNotification& event)> callback,
+    unsigned int flags) const {
+  wpi::SmallString<128> prefixBuf;
+  StringRef prefixStr = prefix.toStringRef(prefixBuf);
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  unsigned int uid = m_notifier.Add(callback, prefixStr, flags);
+  // perform immediate notifications
+  if ((flags & NT_NOTIFY_IMMEDIATE) != 0 && (flags & NT_NOTIFY_NEW) != 0) {
+    for (auto& i : m_entries) {
+      Entry* entry = i.getValue();
+      if (!entry->value || !i.getKey().startswith(prefixStr)) continue;
+      m_notifier.NotifyEntry(entry->local_id, i.getKey(), entry->value,
+                             NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW, uid);
+    }
+  }
+  return uid;
+}
+
+unsigned int Storage::AddListener(
+    unsigned int local_id,
+    std::function<void(const EntryNotification& event)> callback,
+    unsigned int flags) const {
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  unsigned int uid = m_notifier.Add(callback, local_id, flags);
+  // perform immediate notifications
+  if ((flags & NT_NOTIFY_IMMEDIATE) != 0 && (flags & NT_NOTIFY_NEW) != 0 &&
+      local_id < m_localmap.size()) {
+    Entry* entry = m_localmap[local_id].get();
+    if (entry->value) {
+      m_notifier.NotifyEntry(local_id, entry->name, entry->value,
+                             NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW, uid);
+    }
+  }
+  return uid;
+}
+
+unsigned int Storage::AddPolledListener(unsigned int poller,
+                                        const Twine& prefix,
+                                        unsigned int flags) const {
+  wpi::SmallString<128> prefixBuf;
+  StringRef prefixStr = prefix.toStringRef(prefixBuf);
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  unsigned int uid = m_notifier.AddPolled(poller, prefixStr, flags);
+  // perform immediate notifications
+  if ((flags & NT_NOTIFY_IMMEDIATE) != 0 && (flags & NT_NOTIFY_NEW) != 0) {
+    for (auto& i : m_entries) {
+      if (!i.getKey().startswith(prefixStr)) continue;
+      Entry* entry = i.getValue();
+      if (!entry->value) continue;
+      m_notifier.NotifyEntry(entry->local_id, i.getKey(), entry->value,
+                             NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW, uid);
+    }
+  }
+  return uid;
+}
+
+unsigned int Storage::AddPolledListener(unsigned int poller,
+                                        unsigned int local_id,
+                                        unsigned int flags) const {
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  unsigned int uid = m_notifier.AddPolled(poller, local_id, flags);
+  // perform immediate notifications
+  if ((flags & NT_NOTIFY_IMMEDIATE) != 0 && (flags & NT_NOTIFY_NEW) != 0 &&
+      local_id < m_localmap.size()) {
+    Entry* entry = m_localmap[local_id].get();
+    // if no value, don't notify
+    if (entry->value) {
+      m_notifier.NotifyEntry(local_id, entry->name, entry->value,
+                             NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW, uid);
+    }
+  }
+  return uid;
+}
+
+bool Storage::GetPersistentEntries(
+    bool periodic,
+    std::vector<std::pair<std::string, std::shared_ptr<Value>>>* entries)
+    const {
+  // copy values out of storage as quickly as possible so lock isn't held
+  {
+    std::lock_guard<wpi::mutex> lock(m_mutex);
+    // for periodic, don't re-save unless something has changed
+    if (periodic && !m_persistent_dirty) return false;
+    m_persistent_dirty = false;
+    entries->reserve(m_entries.size());
+    for (auto& i : m_entries) {
+      Entry* entry = i.getValue();
+      // only write persistent-flagged values
+      if (!entry->value || !entry->IsPersistent()) continue;
+      entries->emplace_back(i.getKey(), entry->value);
+    }
+  }
+
+  // sort in name order
+  std::sort(entries->begin(), entries->end(),
+            [](const std::pair<std::string, std::shared_ptr<Value>>& a,
+               const std::pair<std::string, std::shared_ptr<Value>>& b) {
+              return a.first < b.first;
+            });
+  return true;
+}
+
+bool Storage::GetEntries(
+    const Twine& prefix,
+    std::vector<std::pair<std::string, std::shared_ptr<Value>>>* entries)
+    const {
+  wpi::SmallString<128> prefixBuf;
+  StringRef prefixStr = prefix.toStringRef(prefixBuf);
+  // copy values out of storage as quickly as possible so lock isn't held
+  {
+    std::lock_guard<wpi::mutex> lock(m_mutex);
+    entries->reserve(m_entries.size());
+    for (auto& i : m_entries) {
+      Entry* entry = i.getValue();
+      // only write values with given prefix
+      if (!entry->value || !i.getKey().startswith(prefixStr)) continue;
+      entries->emplace_back(i.getKey(), entry->value);
+    }
+  }
+
+  // sort in name order
+  std::sort(entries->begin(), entries->end(),
+            [](const std::pair<std::string, std::shared_ptr<Value>>& a,
+               const std::pair<std::string, std::shared_ptr<Value>>& b) {
+              return a.first < b.first;
+            });
+  return true;
+}
+
+void Storage::CreateRpc(unsigned int local_id, StringRef def,
+                        unsigned int rpc_uid) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (local_id >= m_localmap.size()) return;
+  Entry* entry = m_localmap[local_id].get();
+
+  auto old_value = entry->value;
+  auto value = Value::MakeRpc(def);
+  entry->value = value;
+
+  // set up the RPC info
+  entry->rpc_uid = rpc_uid;
+
+  if (old_value && *old_value == *value) return;
+
+  // assign an id if it doesn't have one
+  if (entry->id == 0xffff) {
+    unsigned int id = m_idmap.size();
+    entry->id = id;
+    m_idmap.push_back(entry);
+  }
+
+  // generate message
+  if (!m_dispatcher) return;
+  auto dispatcher = m_dispatcher;
+  if (!old_value || old_value->type() != value->type()) {
+    ++entry->seq_num;
+    auto msg = Message::EntryAssign(
+        entry->name, entry->id, entry->seq_num.value(), value, entry->flags);
+    lock.unlock();
+    dispatcher->QueueOutgoing(msg, nullptr, nullptr);
+  } else {
+    ++entry->seq_num;
+    auto msg = Message::EntryUpdate(entry->id, entry->seq_num.value(), value);
+    lock.unlock();
+    dispatcher->QueueOutgoing(msg, nullptr, nullptr);
+  }
+}
+
+unsigned int Storage::CallRpc(unsigned int local_id, StringRef params) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  if (local_id >= m_localmap.size()) return 0;
+  Entry* entry = m_localmap[local_id].get();
+
+  if (!entry->value || !entry->value->IsRpc()) return 0;
+
+  ++entry->rpc_call_uid;
+  if (entry->rpc_call_uid > 0xffff) entry->rpc_call_uid = 0;
+  unsigned int call_uid = entry->rpc_call_uid;
+
+  auto msg = Message::ExecuteRpc(entry->id, call_uid, params);
+  StringRef name{entry->name};
+
+  if (m_server) {
+    // RPCs are unlikely to be used locally on the server, but handle it
+    // gracefully anyway.
+    auto rpc_uid = entry->rpc_uid;
+    lock.unlock();
+    ConnectionInfo conn_info;
+    conn_info.remote_id = "Server";
+    conn_info.remote_ip = "localhost";
+    conn_info.remote_port = 0;
+    conn_info.last_update = wpi::Now();
+    conn_info.protocol_version = 0x0300;
+    unsigned int call_uid = msg->seq_num_uid();
+    m_rpc_server.ProcessRpc(local_id, call_uid, name, msg->str(), conn_info,
+                            [=](StringRef result) {
+                              std::lock_guard<wpi::mutex> lock(m_mutex);
+                              m_rpc_results.insert(std::make_pair(
+                                  RpcIdPair{local_id, call_uid}, result));
+                              m_rpc_results_cond.notify_all();
+                            },
+                            rpc_uid);
+  } else {
+    auto dispatcher = m_dispatcher;
+    lock.unlock();
+    dispatcher->QueueOutgoing(msg, nullptr, nullptr);
+  }
+  return call_uid;
+}
+
+bool Storage::GetRpcResult(unsigned int local_id, unsigned int call_uid,
+                           std::string* result) {
+  bool timed_out = false;
+  return GetRpcResult(local_id, call_uid, result, -1, &timed_out);
+}
+
+bool Storage::GetRpcResult(unsigned int local_id, unsigned int call_uid,
+                           std::string* result, double timeout,
+                           bool* timed_out) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+
+  RpcIdPair call_pair{local_id, call_uid};
+
+  // only allow one blocking call per rpc call uid
+  if (!m_rpc_blocking_calls.insert(call_pair).second) return false;
+
+  auto timeout_time =
+      std::chrono::steady_clock::now() + std::chrono::duration<double>(timeout);
+  *timed_out = false;
+  for (;;) {
+    auto i = m_rpc_results.find(call_pair);
+    if (i == m_rpc_results.end()) {
+      if (timeout == 0 || m_terminating) {
+        m_rpc_blocking_calls.erase(call_pair);
+        return false;
+      }
+      if (timeout < 0) {
+        m_rpc_results_cond.wait(lock);
+      } else {
+        auto cond_timed_out = m_rpc_results_cond.wait_until(lock, timeout_time);
+        if (cond_timed_out == std::cv_status::timeout) {
+          m_rpc_blocking_calls.erase(call_pair);
+          *timed_out = true;
+          return false;
+        }
+      }
+      // if element does not exist, we have been canceled
+      if (m_rpc_blocking_calls.count(call_pair) == 0) {
+        return false;
+      }
+      if (m_terminating) {
+        m_rpc_blocking_calls.erase(call_pair);
+        return false;
+      }
+      continue;
+    }
+    result->swap(i->getSecond());
+    // safe to erase even if id does not exist
+    m_rpc_blocking_calls.erase(call_pair);
+    m_rpc_results.erase(i);
+    return true;
+  }
+}
+
+void Storage::CancelRpcResult(unsigned int local_id, unsigned int call_uid) {
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  // safe to erase even if id does not exist
+  m_rpc_blocking_calls.erase(RpcIdPair{local_id, call_uid});
+  m_rpc_results_cond.notify_all();
+}
diff --git a/ntcore/src/main/native/cpp/Storage.h b/ntcore/src/main/native/cpp/Storage.h
new file mode 100644
index 0000000..fa9b2bf
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Storage.h
@@ -0,0 +1,262 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_STORAGE_H_
+#define NTCORE_STORAGE_H_
+
+#include <stdint.h>
+
+#include <atomic>
+#include <cstddef>
+#include <functional>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <wpi/DenseMap.h>
+#include <wpi/SmallSet.h>
+#include <wpi/StringMap.h>
+#include <wpi/condition_variable.h>
+#include <wpi/mutex.h>
+
+#include "IStorage.h"
+#include "Message.h"
+#include "SequenceNumber.h"
+#include "ntcore_cpp.h"
+
+namespace wpi {
+class Logger;
+class raw_istream;
+class raw_ostream;
+}  // namespace wpi
+
+namespace nt {
+
+class IEntryNotifier;
+class INetworkConnection;
+class IRpcServer;
+class IStorageTest;
+
+class Storage : public IStorage {
+  friend class StorageTest;
+
+ public:
+  Storage(IEntryNotifier& notifier, IRpcServer& rpcserver, wpi::Logger& logger);
+  Storage(const Storage&) = delete;
+  Storage& operator=(const Storage&) = delete;
+
+  ~Storage();
+
+  // Accessors required by Dispatcher.  An interface is used for
+  // generation of outgoing messages to break a dependency loop between
+  // Storage and Dispatcher.
+  void SetDispatcher(IDispatcher* dispatcher, bool server) override;
+  void ClearDispatcher() override;
+
+  // Required for wire protocol 2.0 to get the entry type of an entry when
+  // receiving entry updates (because the length/type is not provided in the
+  // message itself).  Not used in wire protocol 3.0.
+  NT_Type GetMessageEntryType(unsigned int id) const override;
+
+  void ProcessIncoming(std::shared_ptr<Message> msg, INetworkConnection* conn,
+                       std::weak_ptr<INetworkConnection> conn_weak) override;
+  void GetInitialAssignments(
+      INetworkConnection& conn,
+      std::vector<std::shared_ptr<Message>>* msgs) override;
+  void ApplyInitialAssignments(
+      INetworkConnection& conn, wpi::ArrayRef<std::shared_ptr<Message>> msgs,
+      bool new_server,
+      std::vector<std::shared_ptr<Message>>* out_msgs) override;
+
+  // User functions.  These are the actual implementations of the corresponding
+  // user API functions in ntcore_cpp.
+  std::shared_ptr<Value> GetEntryValue(StringRef name) const;
+  std::shared_ptr<Value> GetEntryValue(unsigned int local_id) const;
+
+  bool SetDefaultEntryValue(StringRef name, std::shared_ptr<Value> value);
+  bool SetDefaultEntryValue(unsigned int local_id,
+                            std::shared_ptr<Value> value);
+
+  bool SetEntryValue(StringRef name, std::shared_ptr<Value> value);
+  bool SetEntryValue(unsigned int local_id, std::shared_ptr<Value> value);
+
+  void SetEntryTypeValue(StringRef name, std::shared_ptr<Value> value);
+  void SetEntryTypeValue(unsigned int local_id, std::shared_ptr<Value> value);
+
+  void SetEntryFlags(StringRef name, unsigned int flags);
+  void SetEntryFlags(unsigned int local_id, unsigned int flags);
+
+  unsigned int GetEntryFlags(StringRef name) const;
+  unsigned int GetEntryFlags(unsigned int local_id) const;
+
+  void DeleteEntry(StringRef name);
+  void DeleteEntry(unsigned int local_id);
+
+  void DeleteAllEntries();
+
+  std::vector<EntryInfo> GetEntryInfo(int inst, const Twine& prefix,
+                                      unsigned int types);
+
+  unsigned int AddListener(
+      const Twine& prefix,
+      std::function<void(const EntryNotification& event)> callback,
+      unsigned int flags) const;
+  unsigned int AddListener(
+      unsigned int local_id,
+      std::function<void(const EntryNotification& event)> callback,
+      unsigned int flags) const;
+
+  unsigned int AddPolledListener(unsigned int poller_uid, const Twine& prefix,
+                                 unsigned int flags) const;
+  unsigned int AddPolledListener(unsigned int poller_uid, unsigned int local_id,
+                                 unsigned int flags) const;
+
+  // Index-only
+  unsigned int GetEntry(const Twine& name);
+  std::vector<unsigned int> GetEntries(const Twine& prefix, unsigned int types);
+  EntryInfo GetEntryInfo(int inst, unsigned int local_id) const;
+  std::string GetEntryName(unsigned int local_id) const;
+  NT_Type GetEntryType(unsigned int local_id) const;
+  uint64_t GetEntryLastChange(unsigned int local_id) const;
+
+  // Filename-based save/load functions.  Used both by periodic saves and
+  // accessible directly via the user API.
+  const char* SavePersistent(const Twine& filename,
+                             bool periodic) const override;
+  const char* LoadPersistent(
+      const Twine& filename,
+      std::function<void(size_t line, const char* msg)> warn) override;
+
+  const char* SaveEntries(const Twine& filename, const Twine& prefix) const;
+  const char* LoadEntries(
+      const Twine& filename, const Twine& prefix,
+      std::function<void(size_t line, const char* msg)> warn);
+
+  // Stream-based save/load functions (exposed for testing purposes).  These
+  // implement the guts of the filename-based functions.
+  void SavePersistent(wpi::raw_ostream& os, bool periodic) const;
+  bool LoadEntries(wpi::raw_istream& is, const Twine& prefix, bool persistent,
+                   std::function<void(size_t line, const char* msg)> warn);
+
+  void SaveEntries(wpi::raw_ostream& os, const Twine& prefix) const;
+
+  // RPC configuration needs to come through here as RPC definitions are
+  // actually special Storage value types.
+  void CreateRpc(unsigned int local_id, StringRef def, unsigned int rpc_uid);
+  unsigned int CallRpc(unsigned int local_id, StringRef params);
+  bool GetRpcResult(unsigned int local_id, unsigned int call_uid,
+                    std::string* result);
+  bool GetRpcResult(unsigned int local_id, unsigned int call_uid,
+                    std::string* result, double timeout, bool* timed_out);
+  void CancelRpcResult(unsigned int local_id, unsigned int call_uid);
+
+ private:
+  // Data for each table entry.
+  struct Entry {
+    explicit Entry(wpi::StringRef name_) : name(name_) {}
+    bool IsPersistent() const { return (flags & NT_PERSISTENT) != 0; }
+
+    // We redundantly store the name so that it's available when accessing the
+    // raw Entry* via the ID map.
+    std::string name;
+
+    // The current value and flags.
+    std::shared_ptr<Value> value;
+    unsigned int flags{0};
+
+    // Unique ID for this entry as used in network messages.  The value is
+    // assigned by the server, so on the client this is 0xffff until an
+    // entry assignment is received back from the server.
+    unsigned int id{0xffff};
+
+    // Local ID.
+    unsigned int local_id{UINT_MAX};
+
+    // Sequence number for update resolution.
+    SequenceNumber seq_num;
+
+    // If value has been written locally.  Used during initial handshake
+    // on client to determine whether or not to accept remote changes.
+    bool local_write{false};
+
+    // RPC handle.
+    unsigned int rpc_uid{UINT_MAX};
+
+    // Last UID used when calling this RPC (primarily for client use).  This
+    // is incremented for each call.
+    unsigned int rpc_call_uid{0};
+  };
+
+  typedef wpi::StringMap<Entry*> EntriesMap;
+  typedef std::vector<Entry*> IdMap;
+  typedef std::vector<std::unique_ptr<Entry>> LocalMap;
+  typedef std::pair<unsigned int, unsigned int> RpcIdPair;
+  typedef wpi::DenseMap<RpcIdPair, std::string> RpcResultMap;
+  typedef wpi::SmallSet<RpcIdPair, 12> RpcBlockingCallSet;
+
+  mutable wpi::mutex m_mutex;
+  EntriesMap m_entries;
+  IdMap m_idmap;
+  LocalMap m_localmap;
+  RpcResultMap m_rpc_results;
+  RpcBlockingCallSet m_rpc_blocking_calls;
+  // If any persistent values have changed
+  mutable bool m_persistent_dirty = false;
+
+  // condition variable and termination flag for blocking on a RPC result
+  std::atomic_bool m_terminating;
+  wpi::condition_variable m_rpc_results_cond;
+
+  // configured by dispatcher at startup
+  IDispatcher* m_dispatcher = nullptr;
+  bool m_server = true;
+
+  IEntryNotifier& m_notifier;
+  IRpcServer& m_rpc_server;
+  wpi::Logger& m_logger;
+
+  void ProcessIncomingEntryAssign(std::shared_ptr<Message> msg,
+                                  INetworkConnection* conn);
+  void ProcessIncomingEntryUpdate(std::shared_ptr<Message> msg,
+                                  INetworkConnection* conn);
+  void ProcessIncomingFlagsUpdate(std::shared_ptr<Message> msg,
+                                  INetworkConnection* conn);
+  void ProcessIncomingEntryDelete(std::shared_ptr<Message> msg,
+                                  INetworkConnection* conn);
+  void ProcessIncomingClearEntries(std::shared_ptr<Message> msg,
+                                   INetworkConnection* conn);
+  void ProcessIncomingExecuteRpc(std::shared_ptr<Message> msg,
+                                 INetworkConnection* conn,
+                                 std::weak_ptr<INetworkConnection> conn_weak);
+  void ProcessIncomingRpcResponse(std::shared_ptr<Message> msg,
+                                  INetworkConnection* conn);
+
+  bool GetPersistentEntries(
+      bool periodic,
+      std::vector<std::pair<std::string, std::shared_ptr<Value>>>* entries)
+      const;
+  bool GetEntries(const Twine& prefix,
+                  std::vector<std::pair<std::string, std::shared_ptr<Value>>>*
+                      entries) const;
+  void SetEntryValueImpl(Entry* entry, std::shared_ptr<Value> value,
+                         std::unique_lock<wpi::mutex>& lock, bool local);
+  void SetEntryFlagsImpl(Entry* entry, unsigned int flags,
+                         std::unique_lock<wpi::mutex>& lock, bool local);
+  void DeleteEntryImpl(Entry* entry, std::unique_lock<wpi::mutex>& lock,
+                       bool local);
+
+  // Must be called with m_mutex held
+  template <typename F>
+  void DeleteAllEntriesImpl(bool local, F should_delete);
+  void DeleteAllEntriesImpl(bool local);
+  Entry* GetOrNew(const Twine& name);
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_STORAGE_H_
diff --git a/ntcore/src/main/native/cpp/Storage_load.cpp b/ntcore/src/main/native/cpp/Storage_load.cpp
new file mode 100644
index 0000000..c05e029
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Storage_load.cpp
@@ -0,0 +1,453 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <cctype>
+#include <string>
+
+#include <wpi/Base64.h>
+#include <wpi/SmallString.h>
+#include <wpi/StringExtras.h>
+#include <wpi/raw_istream.h>
+
+#include "IDispatcher.h"
+#include "IEntryNotifier.h"
+#include "Storage.h"
+
+using namespace nt;
+
+namespace {
+
+class LoadPersistentImpl {
+ public:
+  typedef std::pair<std::string, std::shared_ptr<Value>> Entry;
+  typedef std::function<void(size_t line, const char* msg)> WarnFunc;
+
+  LoadPersistentImpl(wpi::raw_istream& is, WarnFunc warn)
+      : m_is(is), m_warn(warn) {}
+
+  bool Load(StringRef prefix, std::vector<Entry>* entries);
+
+ private:
+  bool ReadLine();
+  bool ReadHeader();
+  NT_Type ReadType();
+  wpi::StringRef ReadName(wpi::SmallVectorImpl<char>& buf);
+  std::shared_ptr<Value> ReadValue(NT_Type type);
+  std::shared_ptr<Value> ReadBooleanValue();
+  std::shared_ptr<Value> ReadDoubleValue();
+  std::shared_ptr<Value> ReadStringValue();
+  std::shared_ptr<Value> ReadRawValue();
+  std::shared_ptr<Value> ReadBooleanArrayValue();
+  std::shared_ptr<Value> ReadDoubleArrayValue();
+  std::shared_ptr<Value> ReadStringArrayValue();
+
+  void Warn(const char* msg) {
+    if (m_warn) m_warn(m_line_num, msg);
+  }
+
+  wpi::raw_istream& m_is;
+  WarnFunc m_warn;
+
+  wpi::StringRef m_line;
+  wpi::SmallString<128> m_line_buf;
+  size_t m_line_num = 0;
+
+  std::vector<int> m_buf_boolean_array;
+  std::vector<double> m_buf_double_array;
+  std::vector<std::string> m_buf_string_array;
+};
+
+}  // namespace
+
+/* Extracts an escaped string token.  Does not unescape the string.
+ * If a string cannot be matched, an empty string is returned.
+ * If the string is unterminated, an empty tail string is returned.
+ * The returned token includes the starting and trailing quotes (unless the
+ * string is unterminated).
+ * Returns a pair containing the extracted token (if any) and the remaining
+ * tail string.
+ */
+static std::pair<wpi::StringRef, wpi::StringRef> ReadStringToken(
+    wpi::StringRef source) {
+  // Match opening quote
+  if (source.empty() || source.front() != '"')
+    return std::make_pair(wpi::StringRef(), source);
+
+  // Scan for ending double quote, checking for escaped as we go.
+  size_t size = source.size();
+  size_t pos;
+  for (pos = 1; pos < size; ++pos) {
+    if (source[pos] == '"' && source[pos - 1] != '\\') {
+      ++pos;  // we want to include the trailing quote in the result
+      break;
+    }
+  }
+  return std::make_pair(source.slice(0, pos), source.substr(pos));
+}
+
+static int fromxdigit(char ch) {
+  if (ch >= 'a' && ch <= 'f')
+    return (ch - 'a' + 10);
+  else if (ch >= 'A' && ch <= 'F')
+    return (ch - 'A' + 10);
+  else
+    return ch - '0';
+}
+
+static wpi::StringRef UnescapeString(wpi::StringRef source,
+                                     wpi::SmallVectorImpl<char>& buf) {
+  assert(source.size() >= 2 && source.front() == '"' && source.back() == '"');
+  buf.clear();
+  buf.reserve(source.size() - 2);
+  for (auto s = source.begin() + 1, end = source.end() - 1; s != end; ++s) {
+    if (*s != '\\') {
+      buf.push_back(*s);
+      continue;
+    }
+    switch (*++s) {
+      case '\\':
+      case '"':
+        buf.push_back(s[-1]);
+        break;
+      case 't':
+        buf.push_back('\t');
+        break;
+      case 'n':
+        buf.push_back('\n');
+        break;
+      case 'x': {
+        if (!isxdigit(*(s + 1))) {
+          buf.push_back('x');  // treat it like a unknown escape
+          break;
+        }
+        int ch = fromxdigit(*++s);
+        if (std::isxdigit(*(s + 1))) {
+          ch <<= 4;
+          ch |= fromxdigit(*++s);
+        }
+        buf.push_back(static_cast<char>(ch));
+        break;
+      }
+      default:
+        buf.push_back(s[-1]);
+        break;
+    }
+  }
+  return wpi::StringRef{buf.data(), buf.size()};
+}
+
+bool LoadPersistentImpl::Load(StringRef prefix, std::vector<Entry>* entries) {
+  if (!ReadHeader()) return false;  // header
+
+  while (ReadLine()) {
+    // type
+    NT_Type type = ReadType();
+    if (type == NT_UNASSIGNED) {
+      Warn("unrecognized type");
+      continue;
+    }
+
+    // name
+    wpi::SmallString<128> buf;
+    wpi::StringRef name = ReadName(buf);
+    if (name.empty() || !name.startswith(prefix)) continue;
+
+    // =
+    m_line = m_line.ltrim(" \t");
+    if (m_line.empty() || m_line.front() != '=') {
+      Warn("expected = after name");
+      continue;
+    }
+    m_line = m_line.drop_front().ltrim(" \t");
+
+    // value
+    auto value = ReadValue(type);
+
+    // move to entries
+    if (value) entries->emplace_back(name, std::move(value));
+  }
+  return true;
+}
+
+bool LoadPersistentImpl::ReadLine() {
+  // ignore blank lines and lines that start with ; or # (comments)
+  while (!m_is.has_error()) {
+    ++m_line_num;
+    m_line = m_is.getline(m_line_buf, INT_MAX).trim();
+    if (!m_line.empty() && m_line.front() != ';' && m_line.front() != '#')
+      return true;
+  }
+  return false;
+}
+
+bool LoadPersistentImpl::ReadHeader() {
+  // header
+  if (!ReadLine() || m_line != "[NetworkTables Storage 3.0]") {
+    Warn("header line mismatch, ignoring rest of file");
+    return false;
+  }
+  return true;
+}
+
+NT_Type LoadPersistentImpl::ReadType() {
+  wpi::StringRef tok;
+  std::tie(tok, m_line) = m_line.split(' ');
+  if (tok == "boolean") {
+    return NT_BOOLEAN;
+  } else if (tok == "double") {
+    return NT_DOUBLE;
+  } else if (tok == "string") {
+    return NT_STRING;
+  } else if (tok == "raw") {
+    return NT_RAW;
+  } else if (tok == "array") {
+    wpi::StringRef array_tok;
+    std::tie(array_tok, m_line) = m_line.split(' ');
+    if (array_tok == "boolean")
+      return NT_BOOLEAN_ARRAY;
+    else if (array_tok == "double")
+      return NT_DOUBLE_ARRAY;
+    else if (array_tok == "string")
+      return NT_STRING_ARRAY;
+  }
+  return NT_UNASSIGNED;
+}
+
+wpi::StringRef LoadPersistentImpl::ReadName(wpi::SmallVectorImpl<char>& buf) {
+  wpi::StringRef tok;
+  std::tie(tok, m_line) = ReadStringToken(m_line);
+  if (tok.empty()) {
+    Warn("missing name");
+    return wpi::StringRef{};
+  }
+  if (tok.back() != '"') {
+    Warn("unterminated name string");
+    return wpi::StringRef{};
+  }
+  return UnescapeString(tok, buf);
+}
+
+std::shared_ptr<Value> LoadPersistentImpl::ReadValue(NT_Type type) {
+  switch (type) {
+    case NT_BOOLEAN:
+      return ReadBooleanValue();
+    case NT_DOUBLE:
+      return ReadDoubleValue();
+    case NT_STRING:
+      return ReadStringValue();
+    case NT_RAW:
+      return ReadRawValue();
+    case NT_BOOLEAN_ARRAY:
+      return ReadBooleanArrayValue();
+    case NT_DOUBLE_ARRAY:
+      return ReadDoubleArrayValue();
+    case NT_STRING_ARRAY:
+      return ReadStringArrayValue();
+    default:
+      return nullptr;
+  }
+}
+
+std::shared_ptr<Value> LoadPersistentImpl::ReadBooleanValue() {
+  // only true or false is accepted
+  if (m_line == "true") return Value::MakeBoolean(true);
+  if (m_line == "false") return Value::MakeBoolean(false);
+  Warn("unrecognized boolean value, not 'true' or 'false'");
+  return nullptr;
+}
+
+std::shared_ptr<Value> LoadPersistentImpl::ReadDoubleValue() {
+  // need to convert to null-terminated string for std::strtod()
+  wpi::SmallString<64> buf = m_line;
+  char* end;
+  double v = std::strtod(buf.c_str(), &end);
+  if (*end != '\0') {
+    Warn("invalid double value");
+    return nullptr;
+  }
+  return Value::MakeDouble(v);
+}
+
+std::shared_ptr<Value> LoadPersistentImpl::ReadStringValue() {
+  wpi::StringRef tok;
+  std::tie(tok, m_line) = ReadStringToken(m_line);
+  if (tok.empty()) {
+    Warn("missing string value");
+    return nullptr;
+  }
+  if (tok.back() != '"') {
+    Warn("unterminated string value");
+    return nullptr;
+  }
+  wpi::SmallString<128> buf;
+  return Value::MakeString(UnescapeString(tok, buf));
+}
+
+std::shared_ptr<Value> LoadPersistentImpl::ReadRawValue() {
+  wpi::SmallString<128> buf;
+  size_t nr;
+  return Value::MakeRaw(wpi::Base64Decode(m_line, &nr, buf));
+}
+
+std::shared_ptr<Value> LoadPersistentImpl::ReadBooleanArrayValue() {
+  m_buf_boolean_array.clear();
+  while (!m_line.empty()) {
+    wpi::StringRef tok;
+    std::tie(tok, m_line) = m_line.split(',');
+    tok = tok.trim(" \t");
+    if (tok == "true") {
+      m_buf_boolean_array.push_back(1);
+    } else if (tok == "false") {
+      m_buf_boolean_array.push_back(0);
+    } else {
+      Warn("unrecognized boolean value, not 'true' or 'false'");
+      return nullptr;
+    }
+  }
+  return Value::MakeBooleanArray(std::move(m_buf_boolean_array));
+}
+
+std::shared_ptr<Value> LoadPersistentImpl::ReadDoubleArrayValue() {
+  m_buf_double_array.clear();
+  while (!m_line.empty()) {
+    wpi::StringRef tok;
+    std::tie(tok, m_line) = m_line.split(',');
+    tok = tok.trim(" \t");
+    // need to convert to null-terminated string for std::strtod()
+    wpi::SmallString<64> buf = tok;
+    char* end;
+    double v = std::strtod(buf.c_str(), &end);
+    if (*end != '\0') {
+      Warn("invalid double value");
+      return nullptr;
+    }
+    m_buf_double_array.push_back(v);
+  }
+
+  return Value::MakeDoubleArray(std::move(m_buf_double_array));
+}
+
+std::shared_ptr<Value> LoadPersistentImpl::ReadStringArrayValue() {
+  m_buf_string_array.clear();
+  while (!m_line.empty()) {
+    wpi::StringRef tok;
+    std::tie(tok, m_line) = ReadStringToken(m_line);
+    if (tok.empty()) {
+      Warn("missing string value");
+      return nullptr;
+    }
+    if (tok.back() != '"') {
+      Warn("unterminated string value");
+      return nullptr;
+    }
+
+    wpi::SmallString<128> buf;
+    m_buf_string_array.push_back(UnescapeString(tok, buf));
+
+    m_line = m_line.ltrim(" \t");
+    if (m_line.empty()) break;
+    if (m_line.front() != ',') {
+      Warn("expected comma between strings");
+      return nullptr;
+    }
+    m_line = m_line.drop_front().ltrim(" \t");
+  }
+
+  return Value::MakeStringArray(std::move(m_buf_string_array));
+}
+
+bool Storage::LoadEntries(
+    wpi::raw_istream& is, const Twine& prefix, bool persistent,
+    std::function<void(size_t line, const char* msg)> warn) {
+  wpi::SmallString<128> prefixBuf;
+  StringRef prefixStr = prefix.toStringRef(prefixBuf);
+
+  // entries to add
+  std::vector<LoadPersistentImpl::Entry> entries;
+
+  // load file
+  if (!LoadPersistentImpl(is, warn).Load(prefixStr, &entries)) return false;
+
+  // copy values into storage as quickly as possible so lock isn't held
+  std::vector<std::shared_ptr<Message>> msgs;
+  std::unique_lock<wpi::mutex> lock(m_mutex);
+  for (auto& i : entries) {
+    Entry* entry = GetOrNew(i.first);
+    auto old_value = entry->value;
+    entry->value = i.second;
+    bool was_persist = entry->IsPersistent();
+    if (!was_persist && persistent) entry->flags |= NT_PERSISTENT;
+
+    // if we're the server, assign an id if it doesn't have one
+    if (m_server && entry->id == 0xffff) {
+      unsigned int id = m_idmap.size();
+      entry->id = id;
+      m_idmap.push_back(entry);
+    }
+
+    // notify (for local listeners)
+    if (m_notifier.local_notifiers()) {
+      if (!old_value) {
+        m_notifier.NotifyEntry(entry->local_id, i.first, i.second,
+                               NT_NOTIFY_NEW | NT_NOTIFY_LOCAL);
+      } else if (*old_value != *i.second) {
+        unsigned int notify_flags = NT_NOTIFY_UPDATE | NT_NOTIFY_LOCAL;
+        if (!was_persist && persistent) notify_flags |= NT_NOTIFY_FLAGS;
+        m_notifier.NotifyEntry(entry->local_id, i.first, i.second,
+                               notify_flags);
+      } else if (!was_persist && persistent) {
+        m_notifier.NotifyEntry(entry->local_id, i.first, i.second,
+                               NT_NOTIFY_FLAGS | NT_NOTIFY_LOCAL);
+      }
+    }
+
+    if (!m_dispatcher) continue;  // shortcut
+    ++entry->seq_num;
+
+    // put on update queue
+    if (!old_value || old_value->type() != i.second->type()) {
+      msgs.emplace_back(Message::EntryAssign(
+          i.first, entry->id, entry->seq_num.value(), i.second, entry->flags));
+    } else if (entry->id != 0xffff) {
+      // don't send an update if we don't have an assigned id yet
+      if (*old_value != *i.second)
+        msgs.emplace_back(
+            Message::EntryUpdate(entry->id, entry->seq_num.value(), i.second));
+      if (!was_persist)
+        msgs.emplace_back(Message::FlagsUpdate(entry->id, entry->flags));
+    }
+  }
+
+  if (m_dispatcher) {
+    auto dispatcher = m_dispatcher;
+    lock.unlock();
+    for (auto& msg : msgs)
+      dispatcher->QueueOutgoing(std::move(msg), nullptr, nullptr);
+  }
+
+  return true;
+}
+
+const char* Storage::LoadPersistent(
+    const Twine& filename,
+    std::function<void(size_t line, const char* msg)> warn) {
+  std::error_code ec;
+  wpi::raw_fd_istream is(filename, ec);
+  if (ec.value() != 0) return "could not open file";
+  if (!LoadEntries(is, "", true, warn)) return "error reading file";
+  return nullptr;
+}
+
+const char* Storage::LoadEntries(
+    const Twine& filename, const Twine& prefix,
+    std::function<void(size_t line, const char* msg)> warn) {
+  std::error_code ec;
+  wpi::raw_fd_istream is(filename, ec);
+  if (ec.value() != 0) return "could not open file";
+  if (!LoadEntries(is, prefix, false, warn)) return "error reading file";
+  return nullptr;
+}
diff --git a/ntcore/src/main/native/cpp/Storage_save.cpp b/ntcore/src/main/native/cpp/Storage_save.cpp
new file mode 100644
index 0000000..3f352da
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Storage_save.cpp
@@ -0,0 +1,272 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <cctype>
+#include <string>
+
+#include <wpi/Base64.h>
+#include <wpi/FileSystem.h>
+#include <wpi/Format.h>
+#include <wpi/SmallString.h>
+#include <wpi/StringExtras.h>
+#include <wpi/raw_ostream.h>
+
+#include "Log.h"
+#include "Storage.h"
+
+using namespace nt;
+
+namespace {
+
+class SavePersistentImpl {
+ public:
+  typedef std::pair<std::string, std::shared_ptr<Value>> Entry;
+
+  explicit SavePersistentImpl(wpi::raw_ostream& os) : m_os(os) {}
+
+  void Save(wpi::ArrayRef<Entry> entries);
+
+ private:
+  void WriteString(wpi::StringRef str);
+  void WriteHeader();
+  void WriteEntries(wpi::ArrayRef<Entry> entries);
+  void WriteEntry(wpi::StringRef name, const Value& value);
+  bool WriteType(NT_Type type);
+  void WriteValue(const Value& value);
+
+  wpi::raw_ostream& m_os;
+};
+
+}  // namespace
+
+/* Escapes and writes a string, including start and end double quotes */
+void SavePersistentImpl::WriteString(wpi::StringRef str) {
+  m_os << '"';
+  for (auto c : str) {
+    switch (c) {
+      case '\\':
+        m_os << "\\\\";
+        break;
+      case '\t':
+        m_os << "\\t";
+        break;
+      case '\n':
+        m_os << "\\n";
+        break;
+      case '"':
+        m_os << "\\\"";
+        break;
+      default:
+        if (std::isprint(c) && c != '=') {
+          m_os << c;
+          break;
+        }
+
+        // Write out the escaped representation.
+        m_os << "\\x";
+        m_os << wpi::hexdigit((c >> 4) & 0xF);
+        m_os << wpi::hexdigit((c >> 0) & 0xF);
+    }
+  }
+  m_os << '"';
+}
+
+void SavePersistentImpl::Save(wpi::ArrayRef<Entry> entries) {
+  WriteHeader();
+  WriteEntries(entries);
+}
+
+void SavePersistentImpl::WriteHeader() {
+  m_os << "[NetworkTables Storage 3.0]\n";
+}
+
+void SavePersistentImpl::WriteEntries(wpi::ArrayRef<Entry> entries) {
+  for (auto& i : entries) {
+    if (!i.second) continue;
+    WriteEntry(i.first, *i.second);
+  }
+}
+
+void SavePersistentImpl::WriteEntry(wpi::StringRef name, const Value& value) {
+  if (!WriteType(value.type())) return;  // type
+  WriteString(name);                     // name
+  m_os << '=';                           // '='
+  WriteValue(value);                     // value
+  m_os << '\n';                          // eol
+}
+
+bool SavePersistentImpl::WriteType(NT_Type type) {
+  switch (type) {
+    case NT_BOOLEAN:
+      m_os << "boolean ";
+      break;
+    case NT_DOUBLE:
+      m_os << "double ";
+      break;
+    case NT_STRING:
+      m_os << "string ";
+      break;
+    case NT_RAW:
+      m_os << "raw ";
+      break;
+    case NT_BOOLEAN_ARRAY:
+      m_os << "array boolean ";
+      break;
+    case NT_DOUBLE_ARRAY:
+      m_os << "array double ";
+      break;
+    case NT_STRING_ARRAY:
+      m_os << "array string ";
+      break;
+    default:
+      return false;
+  }
+  return true;
+}
+
+void SavePersistentImpl::WriteValue(const Value& value) {
+  switch (value.type()) {
+    case NT_BOOLEAN:
+      m_os << (value.GetBoolean() ? "true" : "false");
+      break;
+    case NT_DOUBLE:
+      m_os << wpi::format("%g", value.GetDouble());
+      break;
+    case NT_STRING:
+      WriteString(value.GetString());
+      break;
+    case NT_RAW: {
+      wpi::Base64Encode(m_os, value.GetRaw());
+      break;
+    }
+    case NT_BOOLEAN_ARRAY: {
+      bool first = true;
+      for (auto elem : value.GetBooleanArray()) {
+        if (!first) m_os << ',';
+        first = false;
+        m_os << (elem ? "true" : "false");
+      }
+      break;
+    }
+    case NT_DOUBLE_ARRAY: {
+      bool first = true;
+      for (auto elem : value.GetDoubleArray()) {
+        if (!first) m_os << ',';
+        first = false;
+        m_os << wpi::format("%g", elem);
+      }
+      break;
+    }
+    case NT_STRING_ARRAY: {
+      bool first = true;
+      for (auto& elem : value.GetStringArray()) {
+        if (!first) m_os << ',';
+        first = false;
+        WriteString(elem);
+      }
+      break;
+    }
+    default:
+      break;
+  }
+}
+
+void Storage::SavePersistent(wpi::raw_ostream& os, bool periodic) const {
+  std::vector<SavePersistentImpl::Entry> entries;
+  if (!GetPersistentEntries(periodic, &entries)) return;
+  SavePersistentImpl(os).Save(entries);
+}
+
+const char* Storage::SavePersistent(const Twine& filename,
+                                    bool periodic) const {
+  wpi::SmallString<128> fn;
+  filename.toVector(fn);
+  wpi::SmallString<128> tmp = fn;
+  tmp += ".tmp";
+  wpi::SmallString<128> bak = fn;
+  bak += ".bak";
+
+  // Get entries before creating file
+  std::vector<SavePersistentImpl::Entry> entries;
+  if (!GetPersistentEntries(periodic, &entries)) return nullptr;
+
+  const char* err = nullptr;
+
+  // start by writing to temporary file
+  std::error_code ec;
+  wpi::raw_fd_ostream os(tmp, ec, wpi::sys::fs::F_Text);
+  if (ec.value() != 0) {
+    err = "could not open file";
+    goto done;
+  }
+  DEBUG("saving persistent file '" << filename << "'");
+  SavePersistentImpl(os).Save(entries);
+  os.close();
+  if (os.has_error()) {
+    std::remove(tmp.c_str());
+    err = "error saving file";
+    goto done;
+  }
+
+  // Safely move to real file.  We ignore any failures related to the backup.
+  std::remove(bak.c_str());
+  std::rename(fn.c_str(), bak.c_str());
+  if (std::rename(tmp.c_str(), fn.c_str()) != 0) {
+    std::rename(bak.c_str(), fn.c_str());  // attempt to restore backup
+    err = "could not rename temp file to real file";
+    goto done;
+  }
+
+done:
+  // try again if there was an error
+  if (err && periodic) m_persistent_dirty = true;
+  return err;
+}
+
+void Storage::SaveEntries(wpi::raw_ostream& os, const Twine& prefix) const {
+  std::vector<SavePersistentImpl::Entry> entries;
+  if (!GetEntries(prefix, &entries)) return;
+  SavePersistentImpl(os).Save(entries);
+}
+
+const char* Storage::SaveEntries(const Twine& filename,
+                                 const Twine& prefix) const {
+  wpi::SmallString<128> fn;
+  filename.toVector(fn);
+  wpi::SmallString<128> tmp = fn;
+  tmp += ".tmp";
+  wpi::SmallString<128> bak = fn;
+  bak += ".bak";
+
+  // Get entries before creating file
+  std::vector<SavePersistentImpl::Entry> entries;
+  if (!GetEntries(prefix, &entries)) return nullptr;
+
+  // start by writing to temporary file
+  std::error_code ec;
+  wpi::raw_fd_ostream os(tmp, ec, wpi::sys::fs::F_Text);
+  if (ec.value() != 0) {
+    return "could not open file";
+  }
+  DEBUG("saving file '" << filename << "'");
+  SavePersistentImpl(os).Save(entries);
+  os.close();
+  if (os.has_error()) {
+    std::remove(tmp.c_str());
+    return "error saving file";
+  }
+
+  // Safely move to real file.  We ignore any failures related to the backup.
+  std::remove(bak.c_str());
+  std::rename(fn.c_str(), bak.c_str());
+  if (std::rename(tmp.c_str(), fn.c_str()) != 0) {
+    std::rename(bak.c_str(), fn.c_str());  // attempt to restore backup
+    return "could not rename temp file to real file";
+  }
+
+  return nullptr;
+}
diff --git a/ntcore/src/main/native/cpp/Value.cpp b/ntcore/src/main/native/cpp/Value.cpp
new file mode 100644
index 0000000..61390f0
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Value.cpp
@@ -0,0 +1,228 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <stdint.h>
+
+#include <wpi/memory.h>
+#include <wpi/timestamp.h>
+
+#include "Value_internal.h"
+#include "networktables/NetworkTableValue.h"
+
+using namespace nt;
+
+Value::Value() {
+  m_val.type = NT_UNASSIGNED;
+  m_val.last_change = wpi::Now();
+}
+
+Value::Value(NT_Type type, uint64_t time, const private_init&) {
+  m_val.type = type;
+  if (time == 0)
+    m_val.last_change = wpi::Now();
+  else
+    m_val.last_change = time;
+  if (m_val.type == NT_BOOLEAN_ARRAY)
+    m_val.data.arr_boolean.arr = nullptr;
+  else if (m_val.type == NT_DOUBLE_ARRAY)
+    m_val.data.arr_double.arr = nullptr;
+  else if (m_val.type == NT_STRING_ARRAY)
+    m_val.data.arr_string.arr = nullptr;
+}
+
+Value::~Value() {
+  if (m_val.type == NT_BOOLEAN_ARRAY)
+    delete[] m_val.data.arr_boolean.arr;
+  else if (m_val.type == NT_DOUBLE_ARRAY)
+    delete[] m_val.data.arr_double.arr;
+  else if (m_val.type == NT_STRING_ARRAY)
+    delete[] m_val.data.arr_string.arr;
+}
+
+std::shared_ptr<Value> Value::MakeBooleanArray(wpi::ArrayRef<bool> value,
+                                               uint64_t time) {
+  auto val = std::make_shared<Value>(NT_BOOLEAN_ARRAY, time, private_init());
+  val->m_val.data.arr_boolean.arr = new int[value.size()];
+  val->m_val.data.arr_boolean.size = value.size();
+  std::copy(value.begin(), value.end(), val->m_val.data.arr_boolean.arr);
+  return val;
+}
+
+std::shared_ptr<Value> Value::MakeBooleanArray(wpi::ArrayRef<int> value,
+                                               uint64_t time) {
+  auto val = std::make_shared<Value>(NT_BOOLEAN_ARRAY, time, private_init());
+  val->m_val.data.arr_boolean.arr = new int[value.size()];
+  val->m_val.data.arr_boolean.size = value.size();
+  std::copy(value.begin(), value.end(), val->m_val.data.arr_boolean.arr);
+  return val;
+}
+
+std::shared_ptr<Value> Value::MakeDoubleArray(wpi::ArrayRef<double> value,
+                                              uint64_t time) {
+  auto val = std::make_shared<Value>(NT_DOUBLE_ARRAY, time, private_init());
+  val->m_val.data.arr_double.arr = new double[value.size()];
+  val->m_val.data.arr_double.size = value.size();
+  std::copy(value.begin(), value.end(), val->m_val.data.arr_double.arr);
+  return val;
+}
+
+std::shared_ptr<Value> Value::MakeStringArray(wpi::ArrayRef<std::string> value,
+                                              uint64_t time) {
+  auto val = std::make_shared<Value>(NT_STRING_ARRAY, time, private_init());
+  val->m_string_array = value;
+  // point NT_Value to the contents in the vector.
+  val->m_val.data.arr_string.arr = new NT_String[value.size()];
+  val->m_val.data.arr_string.size = val->m_string_array.size();
+  for (size_t i = 0; i < value.size(); ++i) {
+    val->m_val.data.arr_string.arr[i].str = const_cast<char*>(value[i].c_str());
+    val->m_val.data.arr_string.arr[i].len = value[i].size();
+  }
+  return val;
+}
+
+std::shared_ptr<Value> Value::MakeStringArray(std::vector<std::string>&& value,
+                                              uint64_t time) {
+  auto val = std::make_shared<Value>(NT_STRING_ARRAY, time, private_init());
+  val->m_string_array = std::move(value);
+  value.clear();
+  // point NT_Value to the contents in the vector.
+  val->m_val.data.arr_string.arr = new NT_String[val->m_string_array.size()];
+  val->m_val.data.arr_string.size = val->m_string_array.size();
+  for (size_t i = 0; i < val->m_string_array.size(); ++i) {
+    val->m_val.data.arr_string.arr[i].str =
+        const_cast<char*>(val->m_string_array[i].c_str());
+    val->m_val.data.arr_string.arr[i].len = val->m_string_array[i].size();
+  }
+  return val;
+}
+
+void nt::ConvertToC(const Value& in, NT_Value* out) {
+  out->type = NT_UNASSIGNED;
+  switch (in.type()) {
+    case NT_UNASSIGNED:
+      return;
+    case NT_BOOLEAN:
+      out->data.v_boolean = in.GetBoolean() ? 1 : 0;
+      break;
+    case NT_DOUBLE:
+      out->data.v_double = in.GetDouble();
+      break;
+    case NT_STRING:
+      ConvertToC(in.GetString(), &out->data.v_string);
+      break;
+    case NT_RAW:
+      ConvertToC(in.GetRaw(), &out->data.v_raw);
+      break;
+    case NT_RPC:
+      ConvertToC(in.GetRpc(), &out->data.v_raw);
+      break;
+    case NT_BOOLEAN_ARRAY: {
+      auto v = in.GetBooleanArray();
+      out->data.arr_boolean.arr =
+          static_cast<int*>(wpi::CheckedMalloc(v.size() * sizeof(int)));
+      out->data.arr_boolean.size = v.size();
+      std::copy(v.begin(), v.end(), out->data.arr_boolean.arr);
+      break;
+    }
+    case NT_DOUBLE_ARRAY: {
+      auto v = in.GetDoubleArray();
+      out->data.arr_double.arr =
+          static_cast<double*>(wpi::CheckedMalloc(v.size() * sizeof(double)));
+      out->data.arr_double.size = v.size();
+      std::copy(v.begin(), v.end(), out->data.arr_double.arr);
+      break;
+    }
+    case NT_STRING_ARRAY: {
+      auto v = in.GetStringArray();
+      out->data.arr_string.arr = static_cast<NT_String*>(
+          wpi::CheckedMalloc(v.size() * sizeof(NT_String)));
+      for (size_t i = 0; i < v.size(); ++i)
+        ConvertToC(v[i], &out->data.arr_string.arr[i]);
+      out->data.arr_string.size = v.size();
+      break;
+    }
+    default:
+      // assert(false && "unknown value type");
+      return;
+  }
+  out->type = in.type();
+}
+
+void nt::ConvertToC(wpi::StringRef in, NT_String* out) {
+  out->len = in.size();
+  out->str = static_cast<char*>(wpi::CheckedMalloc(in.size() + 1));
+  std::memcpy(out->str, in.data(), in.size());
+  out->str[in.size()] = '\0';
+}
+
+std::shared_ptr<Value> nt::ConvertFromC(const NT_Value& value) {
+  switch (value.type) {
+    case NT_UNASSIGNED:
+      return nullptr;
+    case NT_BOOLEAN:
+      return Value::MakeBoolean(value.data.v_boolean != 0);
+    case NT_DOUBLE:
+      return Value::MakeDouble(value.data.v_double);
+    case NT_STRING:
+      return Value::MakeString(ConvertFromC(value.data.v_string));
+    case NT_RAW:
+      return Value::MakeRaw(ConvertFromC(value.data.v_raw));
+    case NT_RPC:
+      return Value::MakeRpc(ConvertFromC(value.data.v_raw));
+    case NT_BOOLEAN_ARRAY:
+      return Value::MakeBooleanArray(wpi::ArrayRef<int>(
+          value.data.arr_boolean.arr, value.data.arr_boolean.size));
+    case NT_DOUBLE_ARRAY:
+      return Value::MakeDoubleArray(wpi::ArrayRef<double>(
+          value.data.arr_double.arr, value.data.arr_double.size));
+    case NT_STRING_ARRAY: {
+      std::vector<std::string> v;
+      v.reserve(value.data.arr_string.size);
+      for (size_t i = 0; i < value.data.arr_string.size; ++i)
+        v.push_back(ConvertFromC(value.data.arr_string.arr[i]));
+      return Value::MakeStringArray(std::move(v));
+    }
+    default:
+      // assert(false && "unknown value type");
+      return nullptr;
+  }
+}
+
+bool nt::operator==(const Value& lhs, const Value& rhs) {
+  if (lhs.type() != rhs.type()) return false;
+  switch (lhs.type()) {
+    case NT_UNASSIGNED:
+      return true;  // XXX: is this better being false instead?
+    case NT_BOOLEAN:
+      return lhs.m_val.data.v_boolean == rhs.m_val.data.v_boolean;
+    case NT_DOUBLE:
+      return lhs.m_val.data.v_double == rhs.m_val.data.v_double;
+    case NT_STRING:
+    case NT_RAW:
+    case NT_RPC:
+      return lhs.m_string == rhs.m_string;
+    case NT_BOOLEAN_ARRAY:
+      if (lhs.m_val.data.arr_boolean.size != rhs.m_val.data.arr_boolean.size)
+        return false;
+      return std::memcmp(lhs.m_val.data.arr_boolean.arr,
+                         rhs.m_val.data.arr_boolean.arr,
+                         lhs.m_val.data.arr_boolean.size *
+                             sizeof(lhs.m_val.data.arr_boolean.arr[0])) == 0;
+    case NT_DOUBLE_ARRAY:
+      if (lhs.m_val.data.arr_double.size != rhs.m_val.data.arr_double.size)
+        return false;
+      return std::memcmp(lhs.m_val.data.arr_double.arr,
+                         rhs.m_val.data.arr_double.arr,
+                         lhs.m_val.data.arr_double.size *
+                             sizeof(lhs.m_val.data.arr_double.arr[0])) == 0;
+    case NT_STRING_ARRAY:
+      return lhs.m_string_array == rhs.m_string_array;
+    default:
+      // assert(false && "unknown value type");
+      return false;
+  }
+}
diff --git a/ntcore/src/main/native/cpp/Value_internal.h b/ntcore/src/main/native/cpp/Value_internal.h
new file mode 100644
index 0000000..ea25777
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Value_internal.h
@@ -0,0 +1,31 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_VALUE_INTERNAL_H_
+#define NTCORE_VALUE_INTERNAL_H_
+
+#include <memory>
+#include <string>
+
+#include <wpi/StringRef.h>
+
+#include "ntcore_c.h"
+
+namespace nt {
+
+class Value;
+
+void ConvertToC(const Value& in, NT_Value* out);
+std::shared_ptr<Value> ConvertFromC(const NT_Value& value);
+void ConvertToC(wpi::StringRef in, NT_String* out);
+inline wpi::StringRef ConvertFromC(const NT_String& str) {
+  return wpi::StringRef(str.str, str.len);
+}
+
+}  // namespace nt
+
+#endif  // NTCORE_VALUE_INTERNAL_H_
diff --git a/ntcore/src/main/native/cpp/WireDecoder.cpp b/ntcore/src/main/native/cpp/WireDecoder.cpp
new file mode 100644
index 0000000..132e8a2
--- /dev/null
+++ b/ntcore/src/main/native/cpp/WireDecoder.cpp
@@ -0,0 +1,208 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "WireDecoder.h"
+
+#include <stdint.h>
+
+#include <cassert>
+#include <cstdlib>
+#include <cstring>
+
+#include <wpi/MathExtras.h>
+#include <wpi/leb128.h>
+#include <wpi/memory.h>
+
+using namespace nt;
+
+static double ReadDouble(const char*& buf) {
+  // Fast but non-portable!
+  uint64_t val = (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+  ++buf;
+  val <<= 8;
+  val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+  ++buf;
+  val <<= 8;
+  val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+  ++buf;
+  val <<= 8;
+  val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+  ++buf;
+  val <<= 8;
+  val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+  ++buf;
+  val <<= 8;
+  val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+  ++buf;
+  val <<= 8;
+  val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+  ++buf;
+  val <<= 8;
+  val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+  ++buf;
+  return wpi::BitsToDouble(val);
+}
+
+WireDecoder::WireDecoder(wpi::raw_istream& is, unsigned int proto_rev,
+                         wpi::Logger& logger)
+    : m_is(is), m_logger(logger) {
+  // Start with a 1K temporary buffer.  Use malloc instead of new so we can
+  // realloc.
+  m_allocated = 1024;
+  m_buf = static_cast<char*>(wpi::CheckedMalloc(m_allocated));
+  m_proto_rev = proto_rev;
+  m_error = nullptr;
+}
+
+WireDecoder::~WireDecoder() { std::free(m_buf); }
+
+bool WireDecoder::ReadDouble(double* val) {
+  const char* buf;
+  if (!Read(&buf, 8)) return false;
+  *val = ::ReadDouble(buf);
+  return true;
+}
+
+void WireDecoder::Realloc(size_t len) {
+  // Double current buffer size until we have enough space.
+  if (m_allocated >= len) return;
+  size_t newlen = m_allocated * 2;
+  while (newlen < len) newlen *= 2;
+  m_buf = static_cast<char*>(wpi::CheckedRealloc(m_buf, newlen));
+  m_allocated = newlen;
+}
+
+bool WireDecoder::ReadType(NT_Type* type) {
+  unsigned int itype;
+  if (!Read8(&itype)) return false;
+  // Convert from byte value to enum
+  switch (itype) {
+    case 0x00:
+      *type = NT_BOOLEAN;
+      break;
+    case 0x01:
+      *type = NT_DOUBLE;
+      break;
+    case 0x02:
+      *type = NT_STRING;
+      break;
+    case 0x03:
+      *type = NT_RAW;
+      break;
+    case 0x10:
+      *type = NT_BOOLEAN_ARRAY;
+      break;
+    case 0x11:
+      *type = NT_DOUBLE_ARRAY;
+      break;
+    case 0x12:
+      *type = NT_STRING_ARRAY;
+      break;
+    case 0x20:
+      *type = NT_RPC;
+      break;
+    default:
+      *type = NT_UNASSIGNED;
+      m_error = "unrecognized value type";
+      return false;
+  }
+  return true;
+}
+
+std::shared_ptr<Value> WireDecoder::ReadValue(NT_Type type) {
+  switch (type) {
+    case NT_BOOLEAN: {
+      unsigned int v;
+      if (!Read8(&v)) return nullptr;
+      return Value::MakeBoolean(v != 0);
+    }
+    case NT_DOUBLE: {
+      double v;
+      if (!ReadDouble(&v)) return nullptr;
+      return Value::MakeDouble(v);
+    }
+    case NT_STRING: {
+      std::string v;
+      if (!ReadString(&v)) return nullptr;
+      return Value::MakeString(std::move(v));
+    }
+    case NT_RAW: {
+      if (m_proto_rev < 0x0300u) {
+        m_error = "received raw value in protocol < 3.0";
+        return nullptr;
+      }
+      std::string v;
+      if (!ReadString(&v)) return nullptr;
+      return Value::MakeRaw(std::move(v));
+    }
+    case NT_RPC: {
+      if (m_proto_rev < 0x0300u) {
+        m_error = "received RPC value in protocol < 3.0";
+        return nullptr;
+      }
+      std::string v;
+      if (!ReadString(&v)) return nullptr;
+      return Value::MakeRpc(std::move(v));
+    }
+    case NT_BOOLEAN_ARRAY: {
+      // size
+      unsigned int size;
+      if (!Read8(&size)) return nullptr;
+
+      // array values
+      const char* buf;
+      if (!Read(&buf, size)) return nullptr;
+      std::vector<int> v(size);
+      for (unsigned int i = 0; i < size; ++i) v[i] = buf[i] ? 1 : 0;
+      return Value::MakeBooleanArray(std::move(v));
+    }
+    case NT_DOUBLE_ARRAY: {
+      // size
+      unsigned int size;
+      if (!Read8(&size)) return nullptr;
+
+      // array values
+      const char* buf;
+      if (!Read(&buf, size * 8)) return nullptr;
+      std::vector<double> v(size);
+      for (unsigned int i = 0; i < size; ++i) v[i] = ::ReadDouble(buf);
+      return Value::MakeDoubleArray(std::move(v));
+    }
+    case NT_STRING_ARRAY: {
+      // size
+      unsigned int size;
+      if (!Read8(&size)) return nullptr;
+
+      // array values
+      std::vector<std::string> v(size);
+      for (unsigned int i = 0; i < size; ++i) {
+        if (!ReadString(&v[i])) return nullptr;
+      }
+      return Value::MakeStringArray(std::move(v));
+    }
+    default:
+      m_error = "invalid type when trying to read value";
+      return nullptr;
+  }
+}
+
+bool WireDecoder::ReadString(std::string* str) {
+  size_t len;
+  if (m_proto_rev < 0x0300u) {
+    unsigned int v;
+    if (!Read16(&v)) return false;
+    len = v;
+  } else {
+    uint64_t v;
+    if (!ReadUleb128(&v)) return false;
+    len = v;
+  }
+  const char* buf;
+  if (!Read(&buf, len)) return false;
+  *str = wpi::StringRef(buf, len);
+  return true;
+}
diff --git a/ntcore/src/main/native/cpp/WireDecoder.h b/ntcore/src/main/native/cpp/WireDecoder.h
new file mode 100644
index 0000000..6b4483b
--- /dev/null
+++ b/ntcore/src/main/native/cpp/WireDecoder.h
@@ -0,0 +1,158 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_WIREDECODER_H_
+#define NTCORE_WIREDECODER_H_
+
+#include <stdint.h>
+
+#include <cstddef>
+#include <memory>
+#include <string>
+
+#include <wpi/leb128.h>
+#include <wpi/raw_istream.h>
+
+#include "Log.h"
+#include "networktables/NetworkTableValue.h"
+
+namespace nt {
+
+/* Decodes network data into native representation.
+ * This class is designed to read from a raw_istream, which provides a blocking
+ * read interface.  There are no provisions in this class for resuming a read
+ * that was interrupted partway.  Read functions return false if
+ * raw_istream.read() returned false (indicating the end of the input data
+ * stream).
+ */
+class WireDecoder {
+ public:
+  WireDecoder(wpi::raw_istream& is, unsigned int proto_rev,
+              wpi::Logger& logger);
+  ~WireDecoder();
+
+  void set_proto_rev(unsigned int proto_rev) { m_proto_rev = proto_rev; }
+
+  /* Get the active protocol revision. */
+  unsigned int proto_rev() const { return m_proto_rev; }
+
+  /* Get the logger. */
+  wpi::Logger& logger() const { return m_logger; }
+
+  /* Clears error indicator. */
+  void Reset() { m_error = nullptr; }
+
+  /* Returns error indicator (a string describing the error).  Returns nullptr
+   * if no error has occurred.
+   */
+  const char* error() const { return m_error; }
+
+  void set_error(const char* error) { m_error = error; }
+
+  /* Reads the specified number of bytes.
+   * @param buf pointer to read data (output parameter)
+   * @param len number of bytes to read
+   * Caution: the buffer is only temporarily valid.
+   */
+  bool Read(const char** buf, size_t len) {
+    if (len > m_allocated) Realloc(len);
+    *buf = m_buf;
+    m_is.read(m_buf, len);
+#if 0
+    if (m_logger.min_level() <= NT_LOG_DEBUG4 && m_logger.HasLogger()) {
+      std::ostringstream oss;
+      oss << "read " << len << " bytes:" << std::hex;
+      if (!rv) {
+        oss << "error";
+      } else {
+        for (size_t i = 0; i < len; ++i)
+          oss << ' ' << static_cast<unsigned int>((*buf)[i]);
+      }
+      m_logger.Log(NT_LOG_DEBUG4, __FILE__, __LINE__, oss.str().c_str());
+    }
+#endif
+    return !m_is.has_error();
+  }
+
+  /* Reads a single byte. */
+  bool Read8(unsigned int* val) {
+    const char* buf;
+    if (!Read(&buf, 1)) return false;
+    *val = (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+    return true;
+  }
+
+  /* Reads a 16-bit word. */
+  bool Read16(unsigned int* val) {
+    const char* buf;
+    if (!Read(&buf, 2)) return false;
+    unsigned int v = (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+    ++buf;
+    v <<= 8;
+    v |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+    *val = v;
+    return true;
+  }
+
+  /* Reads a 32-bit word. */
+  bool Read32(uint32_t* val) {
+    const char* buf;
+    if (!Read(&buf, 4)) return false;
+    unsigned int v = (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+    ++buf;
+    v <<= 8;
+    v |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+    ++buf;
+    v <<= 8;
+    v |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+    ++buf;
+    v <<= 8;
+    v |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
+    *val = v;
+    return true;
+  }
+
+  /* Reads a double. */
+  bool ReadDouble(double* val);
+
+  /* Reads an ULEB128-encoded unsigned integer. */
+  bool ReadUleb128(uint64_t* val) { return wpi::ReadUleb128(m_is, val); }
+
+  bool ReadType(NT_Type* type);
+  bool ReadString(std::string* str);
+  std::shared_ptr<Value> ReadValue(NT_Type type);
+
+  WireDecoder(const WireDecoder&) = delete;
+  WireDecoder& operator=(const WireDecoder&) = delete;
+
+ protected:
+  /* The protocol revision.  E.g. 0x0200 for version 2.0. */
+  unsigned int m_proto_rev;
+
+  /* Error indicator. */
+  const char* m_error;
+
+ private:
+  /* Reallocate temporary buffer to specified length. */
+  void Realloc(size_t len);
+
+  /* input stream */
+  wpi::raw_istream& m_is;
+
+  /* logger */
+  wpi::Logger& m_logger;
+
+  /* temporary buffer */
+  char* m_buf;
+
+  /* allocated size of temporary buffer */
+  size_t m_allocated;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_WIREDECODER_H_
diff --git a/ntcore/src/main/native/cpp/WireEncoder.cpp b/ntcore/src/main/native/cpp/WireEncoder.cpp
new file mode 100644
index 0000000..6538349
--- /dev/null
+++ b/ntcore/src/main/native/cpp/WireEncoder.cpp
@@ -0,0 +1,199 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "WireEncoder.h"
+
+#include <stdint.h>
+
+#include <cassert>
+#include <cstdlib>
+#include <cstring>
+
+#include <wpi/MathExtras.h>
+#include <wpi/leb128.h>
+
+using namespace nt;
+
+WireEncoder::WireEncoder(unsigned int proto_rev) {
+  m_proto_rev = proto_rev;
+  m_error = nullptr;
+}
+
+void WireEncoder::WriteDouble(double val) {
+  // The highest performance way to do this, albeit non-portable.
+  uint64_t v = wpi::DoubleToBits(val);
+  m_data.append(
+      {static_cast<char>((v >> 56) & 0xff), static_cast<char>((v >> 48) & 0xff),
+       static_cast<char>((v >> 40) & 0xff), static_cast<char>((v >> 32) & 0xff),
+       static_cast<char>((v >> 24) & 0xff), static_cast<char>((v >> 16) & 0xff),
+       static_cast<char>((v >> 8) & 0xff), static_cast<char>(v & 0xff)});
+}
+
+void WireEncoder::WriteUleb128(uint32_t val) { wpi::WriteUleb128(m_data, val); }
+
+void WireEncoder::WriteType(NT_Type type) {
+  char ch;
+  // Convert from enum to actual byte value.
+  switch (type) {
+    case NT_BOOLEAN:
+      ch = 0x00;
+      break;
+    case NT_DOUBLE:
+      ch = 0x01;
+      break;
+    case NT_STRING:
+      ch = 0x02;
+      break;
+    case NT_RAW:
+      if (m_proto_rev < 0x0300u) {
+        m_error = "raw type not supported in protocol < 3.0";
+        return;
+      }
+      ch = 0x03;
+      break;
+    case NT_BOOLEAN_ARRAY:
+      ch = 0x10;
+      break;
+    case NT_DOUBLE_ARRAY:
+      ch = 0x11;
+      break;
+    case NT_STRING_ARRAY:
+      ch = 0x12;
+      break;
+    case NT_RPC:
+      if (m_proto_rev < 0x0300u) {
+        m_error = "RPC type not supported in protocol < 3.0";
+        return;
+      }
+      ch = 0x20;
+      break;
+    default:
+      m_error = "unrecognized type";
+      return;
+  }
+  m_data.push_back(ch);
+}
+
+size_t WireEncoder::GetValueSize(const Value& value) const {
+  switch (value.type()) {
+    case NT_BOOLEAN:
+      return 1;
+    case NT_DOUBLE:
+      return 8;
+    case NT_STRING:
+      return GetStringSize(value.GetString());
+    case NT_RAW:
+      if (m_proto_rev < 0x0300u) return 0;
+      return GetStringSize(value.GetRaw());
+    case NT_RPC:
+      if (m_proto_rev < 0x0300u) return 0;
+      return GetStringSize(value.GetRpc());
+    case NT_BOOLEAN_ARRAY: {
+      // 1-byte size, 1 byte per element
+      size_t size = value.GetBooleanArray().size();
+      if (size > 0xff) size = 0xff;  // size is only 1 byte, truncate
+      return 1 + size;
+    }
+    case NT_DOUBLE_ARRAY: {
+      // 1-byte size, 8 bytes per element
+      size_t size = value.GetDoubleArray().size();
+      if (size > 0xff) size = 0xff;  // size is only 1 byte, truncate
+      return 1 + size * 8;
+    }
+    case NT_STRING_ARRAY: {
+      auto v = value.GetStringArray();
+      size_t size = v.size();
+      if (size > 0xff) size = 0xff;  // size is only 1 byte, truncate
+      size_t len = 1;                // 1-byte size
+      for (size_t i = 0; i < size; ++i) len += GetStringSize(v[i]);
+      return len;
+    }
+    default:
+      return 0;
+  }
+}
+
+void WireEncoder::WriteValue(const Value& value) {
+  switch (value.type()) {
+    case NT_BOOLEAN:
+      Write8(value.GetBoolean() ? 1 : 0);
+      break;
+    case NT_DOUBLE:
+      WriteDouble(value.GetDouble());
+      break;
+    case NT_STRING:
+      WriteString(value.GetString());
+      break;
+    case NT_RAW:
+      if (m_proto_rev < 0x0300u) {
+        m_error = "raw values not supported in protocol < 3.0";
+        return;
+      }
+      WriteString(value.GetRaw());
+      break;
+    case NT_RPC:
+      if (m_proto_rev < 0x0300u) {
+        m_error = "RPC values not supported in protocol < 3.0";
+        return;
+      }
+      WriteString(value.GetRpc());
+      break;
+    case NT_BOOLEAN_ARRAY: {
+      auto v = value.GetBooleanArray();
+      size_t size = v.size();
+      if (size > 0xff) size = 0xff;  // size is only 1 byte, truncate
+      Write8(size);
+
+      for (size_t i = 0; i < size; ++i) Write8(v[i] ? 1 : 0);
+      break;
+    }
+    case NT_DOUBLE_ARRAY: {
+      auto v = value.GetDoubleArray();
+      size_t size = v.size();
+      if (size > 0xff) size = 0xff;  // size is only 1 byte, truncate
+      Write8(size);
+
+      for (size_t i = 0; i < size; ++i) WriteDouble(v[i]);
+      break;
+    }
+    case NT_STRING_ARRAY: {
+      auto v = value.GetStringArray();
+      size_t size = v.size();
+      if (size > 0xff) size = 0xff;  // size is only 1 byte, truncate
+      Write8(size);
+
+      for (size_t i = 0; i < size; ++i) WriteString(v[i]);
+      break;
+    }
+    default:
+      m_error = "unrecognized type when writing value";
+      return;
+  }
+}
+
+size_t WireEncoder::GetStringSize(wpi::StringRef str) const {
+  if (m_proto_rev < 0x0300u) {
+    size_t len = str.size();
+    if (len > 0xffff) len = 0xffff;  // Limited to 64K length; truncate
+    return 2 + len;
+  }
+  return wpi::SizeUleb128(str.size()) + str.size();
+}
+
+void WireEncoder::WriteString(wpi::StringRef str) {
+  // length
+  size_t len = str.size();
+  if (m_proto_rev < 0x0300u) {
+    if (len > 0xffff) len = 0xffff;  // Limited to 64K length; truncate
+    Write16(len);
+  } else {
+    WriteUleb128(len);
+  }
+
+  // contents
+  m_data.append(str.data(), str.data() + len);
+}
diff --git a/ntcore/src/main/native/cpp/WireEncoder.h b/ntcore/src/main/native/cpp/WireEncoder.h
new file mode 100644
index 0000000..c4f769c
--- /dev/null
+++ b/ntcore/src/main/native/cpp/WireEncoder.h
@@ -0,0 +1,111 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_WIREENCODER_H_
+#define NTCORE_WIREENCODER_H_
+
+#include <stdint.h>
+
+#include <cassert>
+#include <cstddef>
+
+#include <wpi/SmallVector.h>
+#include <wpi/StringRef.h>
+
+#include "networktables/NetworkTableValue.h"
+
+namespace nt {
+
+/* Encodes native data for network transmission.
+ * This class maintains an internal memory buffer for written data so that
+ * it can be efficiently bursted to the network after a number of writes
+ * have been performed.  For this reason, all operations are non-blocking.
+ */
+class WireEncoder {
+ public:
+  explicit WireEncoder(unsigned int proto_rev);
+
+  /* Change the protocol revision (mostly affects value encoding). */
+  void set_proto_rev(unsigned int proto_rev) { m_proto_rev = proto_rev; }
+
+  /* Get the active protocol revision. */
+  unsigned int proto_rev() const { return m_proto_rev; }
+
+  /* Clears buffer and error indicator. */
+  void Reset() {
+    m_data.clear();
+    m_error = nullptr;
+  }
+
+  /* Returns error indicator (a string describing the error).  Returns nullptr
+   * if no error has occurred.
+   */
+  const char* error() const { return m_error; }
+
+  /* Returns pointer to start of memory buffer with written data. */
+  const char* data() const { return m_data.data(); }
+
+  /* Returns number of bytes written to memory buffer. */
+  size_t size() const { return m_data.size(); }
+
+  wpi::StringRef ToStringRef() const {
+    return wpi::StringRef(m_data.data(), m_data.size());
+  }
+
+  /* Writes a single byte. */
+  void Write8(unsigned int val) {
+    m_data.push_back(static_cast<char>(val & 0xff));
+  }
+
+  /* Writes a 16-bit word. */
+  void Write16(unsigned int val) {
+    m_data.append(
+        {static_cast<char>((val >> 8) & 0xff), static_cast<char>(val & 0xff)});
+  }
+
+  /* Writes a 32-bit word. */
+  void Write32(uint32_t val) {
+    m_data.append({static_cast<char>((val >> 24) & 0xff),
+                   static_cast<char>((val >> 16) & 0xff),
+                   static_cast<char>((val >> 8) & 0xff),
+                   static_cast<char>(val & 0xff)});
+  }
+
+  /* Writes a double. */
+  void WriteDouble(double val);
+
+  /* Writes an ULEB128-encoded unsigned integer. */
+  void WriteUleb128(uint32_t val);
+
+  void WriteType(NT_Type type);
+  void WriteValue(const Value& value);
+  void WriteString(wpi::StringRef str);
+
+  /* Utility function to get the written size of a value (without actually
+   * writing it).
+   */
+  size_t GetValueSize(const Value& value) const;
+
+  /* Utility function to get the written size of a string (without actually
+   * writing it).
+   */
+  size_t GetStringSize(wpi::StringRef str) const;
+
+ protected:
+  /* The protocol revision.  E.g. 0x0200 for version 2.0. */
+  unsigned int m_proto_rev;
+
+  /* Error indicator. */
+  const char* m_error;
+
+ private:
+  wpi::SmallVector<char, 256> m_data;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_WIREENCODER_H_
diff --git a/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp b/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp
new file mode 100644
index 0000000..49f34f0
--- /dev/null
+++ b/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp
@@ -0,0 +1,1874 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2018 FIRST. All Rights Reserved.                             */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <jni.h>
+
+#include <cassert>
+
+#include <wpi/ConvertUTF.h>
+#include <wpi/SmallString.h>
+#include <wpi/jni_util.h>
+#include <wpi/raw_ostream.h>
+
+#include "edu_wpi_first_networktables_NetworkTablesJNI.h"
+#include "ntcore.h"
+
+using namespace wpi::java;
+
+#ifdef __GNUC__
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
+//
+// Globals and load/unload
+//
+
+// Used for callback.
+static JavaVM* jvm = nullptr;
+static JClass booleanCls;
+static JClass connectionInfoCls;
+static JClass connectionNotificationCls;
+static JClass doubleCls;
+static JClass entryInfoCls;
+static JClass entryNotificationCls;
+static JClass logMessageCls;
+static JClass rpcAnswerCls;
+static JClass valueCls;
+static JException illegalArgEx;
+static JException interruptedEx;
+static JException nullPointerEx;
+static JException persistentEx;
+
+static const JClassInit classes[] = {
+    {"java/lang/Boolean", &booleanCls},
+    {"edu/wpi/first/networktables/ConnectionInfo", &connectionInfoCls},
+    {"edu/wpi/first/networktables/ConnectionNotification",
+     &connectionNotificationCls},
+    {"java/lang/Double", &doubleCls},
+    {"edu/wpi/first/networktables/EntryInfo", &entryInfoCls},
+    {"edu/wpi/first/networktables/EntryNotification", &entryNotificationCls},
+    {"edu/wpi/first/networktables/LogMessage", &logMessageCls},
+    {"edu/wpi/first/networktables/RpcAnswer", &rpcAnswerCls},
+    {"edu/wpi/first/networktables/NetworkTableValue", &valueCls}};
+
+static const JExceptionInit exceptions[] = {
+    {"java/lang/IllegalArgumentException", &illegalArgEx},
+    {"java/lang/InterruptedException", &interruptedEx},
+    {"java/lang/NullPointerException", &nullPointerEx},
+    {"edu/wpi/first/networktables/PersistentException", &persistentEx}};
+
+extern "C" {
+
+JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
+  jvm = vm;
+
+  JNIEnv* env;
+  if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK)
+    return JNI_ERR;
+
+  // Cache references to classes
+  for (auto& c : classes) {
+    *c.cls = JClass(env, c.name);
+    if (!*c.cls) return JNI_ERR;
+  }
+
+  for (auto& c : exceptions) {
+    *c.cls = JException(env, c.name);
+    if (!*c.cls) return JNI_ERR;
+  }
+
+  return JNI_VERSION_1_6;
+}
+
+JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) {
+  JNIEnv* env;
+  if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK)
+    return;
+  // Delete global references
+  for (auto& c : classes) {
+    c.cls->free(env);
+  }
+  for (auto& c : exceptions) {
+    c.cls->free(env);
+  }
+  jvm = nullptr;
+}
+
+}  // extern "C"
+
+//
+// Conversions from Java objects to C++
+//
+
+inline std::shared_ptr<nt::Value> FromJavaRaw(JNIEnv* env, jbyteArray jarr,
+                                              jlong time) {
+  CriticalJByteArrayRef ref{env, jarr};
+  if (!ref) return nullptr;
+  return nt::Value::MakeRaw(ref, time);
+}
+
+inline std::shared_ptr<nt::Value> FromJavaRawBB(JNIEnv* env, jobject jbb,
+                                                int len, jlong time) {
+  JByteArrayRef ref{env, jbb, len};
+  if (!ref) return nullptr;
+  return nt::Value::MakeRaw(ref.str(), time);
+}
+
+inline std::shared_ptr<nt::Value> FromJavaRpc(JNIEnv* env, jbyteArray jarr,
+                                              jlong time) {
+  CriticalJByteArrayRef ref{env, jarr};
+  if (!ref) return nullptr;
+  return nt::Value::MakeRpc(ref.str(), time);
+}
+
+std::shared_ptr<nt::Value> FromJavaBooleanArray(JNIEnv* env, jbooleanArray jarr,
+                                                jlong time) {
+  CriticalJBooleanArrayRef ref{env, jarr};
+  if (!ref) return nullptr;
+  wpi::ArrayRef<jboolean> elements{ref};
+  size_t len = elements.size();
+  std::vector<int> arr;
+  arr.reserve(len);
+  for (size_t i = 0; i < len; ++i) arr.push_back(elements[i]);
+  return nt::Value::MakeBooleanArray(arr, time);
+}
+
+std::shared_ptr<nt::Value> FromJavaDoubleArray(JNIEnv* env, jdoubleArray jarr,
+                                               jlong time) {
+  CriticalJDoubleArrayRef ref{env, jarr};
+  if (!ref) return nullptr;
+  return nt::Value::MakeDoubleArray(ref, time);
+}
+
+std::shared_ptr<nt::Value> FromJavaStringArray(JNIEnv* env, jobjectArray jarr,
+                                               jlong time) {
+  size_t len = env->GetArrayLength(jarr);
+  std::vector<std::string> arr;
+  arr.reserve(len);
+  for (size_t i = 0; i < len; ++i) {
+    JLocal<jstring> elem{
+        env, static_cast<jstring>(env->GetObjectArrayElement(jarr, i))};
+    if (!elem) return nullptr;
+    arr.push_back(JStringRef{env, elem}.str());
+  }
+  return nt::Value::MakeStringArray(std::move(arr), time);
+}
+
+//
+// Conversions from C++ to Java objects
+//
+
+static jobject MakeJObject(JNIEnv* env, const nt::Value& value) {
+  static jmethodID booleanConstructor = nullptr;
+  static jmethodID doubleConstructor = nullptr;
+  if (!booleanConstructor)
+    booleanConstructor = env->GetMethodID(booleanCls, "<init>", "(Z)V");
+  if (!doubleConstructor)
+    doubleConstructor = env->GetMethodID(doubleCls, "<init>", "(D)V");
+
+  switch (value.type()) {
+    case NT_BOOLEAN:
+      return env->NewObject(booleanCls, booleanConstructor,
+                            (jboolean)(value.GetBoolean() ? 1 : 0));
+    case NT_DOUBLE:
+      return env->NewObject(doubleCls, doubleConstructor,
+                            (jdouble)value.GetDouble());
+    case NT_STRING:
+      return MakeJString(env, value.GetString());
+    case NT_RAW:
+      return MakeJByteArray(env, value.GetRaw());
+    case NT_BOOLEAN_ARRAY:
+      return MakeJBooleanArray(env, value.GetBooleanArray());
+    case NT_DOUBLE_ARRAY:
+      return MakeJDoubleArray(env, value.GetDoubleArray());
+    case NT_STRING_ARRAY:
+      return MakeJStringArray(env, value.GetStringArray());
+    case NT_RPC:
+      return MakeJByteArray(env, value.GetRpc());
+    default:
+      return nullptr;
+  }
+}
+
+static jobject MakeJValue(JNIEnv* env, const nt::Value* value) {
+  static jmethodID constructor =
+      env->GetMethodID(valueCls, "<init>", "(ILjava/lang/Object;J)V");
+  if (!value)
+    return env->NewObject(valueCls, constructor, (jint)NT_UNASSIGNED, nullptr,
+                          (jlong)0);
+  return env->NewObject(valueCls, constructor, (jint)value->type(),
+                        MakeJObject(env, *value), (jlong)value->time());
+}
+
+static jobject MakeJObject(JNIEnv* env, const nt::ConnectionInfo& info) {
+  static jmethodID constructor =
+      env->GetMethodID(connectionInfoCls, "<init>",
+                       "(Ljava/lang/String;Ljava/lang/String;IJI)V");
+  JLocal<jstring> remote_id{env, MakeJString(env, info.remote_id)};
+  JLocal<jstring> remote_ip{env, MakeJString(env, info.remote_ip)};
+  return env->NewObject(connectionInfoCls, constructor, remote_id.obj(),
+                        remote_ip.obj(), (jint)info.remote_port,
+                        (jlong)info.last_update, (jint)info.protocol_version);
+}
+
+static jobject MakeJObject(JNIEnv* env, jobject inst,
+                           const nt::ConnectionNotification& notification) {
+  static jmethodID constructor = env->GetMethodID(
+      connectionNotificationCls, "<init>",
+      "(Ledu/wpi/first/networktables/NetworkTableInstance;IZLedu/wpi/first/"
+      "networktables/ConnectionInfo;)V");
+  JLocal<jobject> conn{env, MakeJObject(env, notification.conn)};
+  return env->NewObject(connectionNotificationCls, constructor, inst,
+                        (jint)notification.listener,
+                        (jboolean)notification.connected, conn.obj());
+}
+
+static jobject MakeJObject(JNIEnv* env, jobject inst,
+                           const nt::EntryInfo& info) {
+  static jmethodID constructor =
+      env->GetMethodID(entryInfoCls, "<init>",
+                       "(Ledu/wpi/first/networktables/"
+                       "NetworkTableInstance;ILjava/lang/String;IIJ)V");
+  JLocal<jstring> name{env, MakeJString(env, info.name)};
+  return env->NewObject(entryInfoCls, constructor, inst, (jint)info.entry,
+                        name.obj(), (jint)info.type, (jint)info.flags,
+                        (jlong)info.last_change);
+}
+
+static jobject MakeJObject(JNIEnv* env, jobject inst,
+                           const nt::EntryNotification& notification) {
+  static jmethodID constructor = env->GetMethodID(
+      entryNotificationCls, "<init>",
+      "(Ledu/wpi/first/networktables/NetworkTableInstance;IILjava/lang/"
+      "String;Ledu/wpi/first/networktables/NetworkTableValue;I)V");
+  JLocal<jstring> name{env, MakeJString(env, notification.name)};
+  JLocal<jobject> value{env, MakeJValue(env, notification.value.get())};
+  return env->NewObject(entryNotificationCls, constructor, inst,
+                        (jint)notification.listener, (jint)notification.entry,
+                        name.obj(), value.obj(), (jint)notification.flags);
+}
+
+static jobject MakeJObject(JNIEnv* env, jobject inst,
+                           const nt::LogMessage& msg) {
+  static jmethodID constructor = env->GetMethodID(
+      logMessageCls, "<init>",
+      "(Ledu/wpi/first/networktables/NetworkTableInstance;IILjava/lang/"
+      "String;ILjava/lang/String;)V");
+  JLocal<jstring> filename{env, MakeJString(env, msg.filename)};
+  JLocal<jstring> message{env, MakeJString(env, msg.message)};
+  return env->NewObject(logMessageCls, constructor, inst, (jint)msg.logger,
+                        (jint)msg.level, filename.obj(), (jint)msg.line,
+                        message.obj());
+}
+
+static jobject MakeJObject(JNIEnv* env, jobject inst,
+                           const nt::RpcAnswer& answer) {
+  static jmethodID constructor =
+      env->GetMethodID(rpcAnswerCls, "<init>",
+                       "(Ledu/wpi/first/networktables/"
+                       "NetworkTableInstance;IILjava/lang/String;[B"
+                       "Ledu/wpi/first/networktables/ConnectionInfo;)V");
+  JLocal<jstring> name{env, MakeJString(env, answer.name)};
+  JLocal<jbyteArray> params{env, MakeJByteArray(env, answer.params)};
+  JLocal<jobject> conn{env, MakeJObject(env, answer.conn)};
+  return env->NewObject(rpcAnswerCls, constructor, inst, (jint)answer.entry,
+                        (jint)answer.call, name.obj(), params.obj(),
+                        conn.obj());
+}
+
+static jobjectArray MakeJObject(JNIEnv* env, jobject inst,
+                                wpi::ArrayRef<nt::ConnectionNotification> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), connectionNotificationCls, nullptr);
+  if (!jarr) return nullptr;
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, inst, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObject(JNIEnv* env, jobject inst,
+                                wpi::ArrayRef<nt::EntryNotification> arr) {
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), entryNotificationCls, nullptr);
+  if (!jarr) return nullptr;
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, inst, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObject(JNIEnv* env, jobject inst,
+                                wpi::ArrayRef<nt::LogMessage> arr) {
+  jobjectArray jarr = env->NewObjectArray(arr.size(), logMessageCls, nullptr);
+  if (!jarr) return nullptr;
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, inst, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+static jobjectArray MakeJObject(JNIEnv* env, jobject inst,
+                                wpi::ArrayRef<nt::RpcAnswer> arr) {
+  jobjectArray jarr = env->NewObjectArray(arr.size(), rpcAnswerCls, nullptr);
+  if (!jarr) return nullptr;
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> elem{env, MakeJObject(env, inst, arr[i])};
+    env->SetObjectArrayElement(jarr, i, elem.obj());
+  }
+  return jarr;
+}
+
+extern "C" {
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getDefaultInstance
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getDefaultInstance
+  (JNIEnv*, jclass)
+{
+  return nt::GetDefaultInstance();
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    createInstance
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_createInstance
+  (JNIEnv*, jclass)
+{
+  return nt::CreateInstance();
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    destroyInstance
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_destroyInstance
+  (JNIEnv*, jclass, jint inst)
+{
+  nt::DestroyInstance(inst);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getInstanceFromHandle
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getInstanceFromHandle
+  (JNIEnv*, jclass, jint handle)
+{
+  return nt::GetInstanceFromHandle(handle);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getEntry
+ * Signature: (ILjava/lang/String;)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntry
+  (JNIEnv* env, jclass, jint inst, jstring key)
+{
+  if (!key) {
+    nullPointerEx.Throw(env, "key cannot be null");
+    return false;
+  }
+  return nt::GetEntry(inst, JStringRef{env, key}.str());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getEntries
+ * Signature: (ILjava/lang/String;I)[I
+ */
+JNIEXPORT jintArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntries
+  (JNIEnv* env, jclass, jint inst, jstring prefix, jint types)
+{
+  if (!prefix) {
+    nullPointerEx.Throw(env, "prefix cannot be null");
+    return nullptr;
+  }
+  return MakeJIntArray(
+      env, nt::GetEntries(inst, JStringRef{env, prefix}.str(), types));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getEntryName
+ * Signature: (I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntryName
+  (JNIEnv* env, jclass, jint entry)
+{
+  return MakeJString(env, nt::GetEntryName(entry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getEntryLastChange
+ * Signature: (I)J
+ */
+JNIEXPORT jlong JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntryLastChange
+  (JNIEnv*, jclass, jint entry)
+{
+  return nt::GetEntryLastChange(entry);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getType
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getType
+  (JNIEnv*, jclass, jint entry)
+{
+  return nt::GetEntryType(entry);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setBoolean
+ * Signature: (IJZZ)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setBoolean
+  (JNIEnv*, jclass, jint entry, jlong time, jboolean value, jboolean force)
+{
+  if (force) {
+    nt::SetEntryTypeValue(entry,
+                          nt::Value::MakeBoolean(value != JNI_FALSE, time));
+    return JNI_TRUE;
+  }
+  return nt::SetEntryValue(entry,
+                           nt::Value::MakeBoolean(value != JNI_FALSE, time));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDouble
+ * Signature: (IJDZ)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDouble
+  (JNIEnv*, jclass, jint entry, jlong time, jdouble value, jboolean force)
+{
+  if (force) {
+    nt::SetEntryTypeValue(entry, nt::Value::MakeDouble(value, time));
+    return JNI_TRUE;
+  }
+  return nt::SetEntryValue(entry, nt::Value::MakeDouble(value, time));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setString
+ * Signature: (IJLjava/lang/String;Z)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setString
+  (JNIEnv* env, jclass, jint entry, jlong time, jstring value, jboolean force)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value cannot be null");
+    return false;
+  }
+  if (force) {
+    nt::SetEntryTypeValue(
+        entry, nt::Value::MakeString(JStringRef{env, value}.str(), time));
+    return JNI_TRUE;
+  }
+  return nt::SetEntryValue(
+      entry, nt::Value::MakeString(JStringRef{env, value}.str(), time));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setRaw
+ * Signature: (IJ[BZ)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setRaw__IJ_3BZ
+  (JNIEnv* env, jclass, jint entry, jlong time, jbyteArray value,
+   jboolean force)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value cannot be null");
+    return false;
+  }
+  auto v = FromJavaRaw(env, value, time);
+  if (!v) return false;
+  if (force) {
+    nt::SetEntryTypeValue(entry, v);
+    return JNI_TRUE;
+  }
+  return nt::SetEntryValue(entry, v);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setRaw
+ * Signature: (IJLjava/lang/Object;IZ)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setRaw__IJLjava_nio_ByteBuffer_2IZ
+  (JNIEnv* env, jclass, jint entry, jlong time, jobject value, jint len,
+   jboolean force)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value cannot be null");
+    return false;
+  }
+  auto v = FromJavaRawBB(env, value, len, time);
+  if (!v) return false;
+  if (force) {
+    nt::SetEntryTypeValue(entry, v);
+    return JNI_TRUE;
+  }
+  return nt::SetEntryValue(entry, v);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setBooleanArray
+ * Signature: (IJ[ZZ)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setBooleanArray
+  (JNIEnv* env, jclass, jint entry, jlong time, jbooleanArray value,
+   jboolean force)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value cannot be null");
+    return false;
+  }
+  auto v = FromJavaBooleanArray(env, value, time);
+  if (!v) return false;
+  if (force) {
+    nt::SetEntryTypeValue(entry, v);
+    return JNI_TRUE;
+  }
+  return nt::SetEntryValue(entry, v);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDoubleArray
+ * Signature: (IJ[DZ)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDoubleArray
+  (JNIEnv* env, jclass, jint entry, jlong time, jdoubleArray value,
+   jboolean force)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value cannot be null");
+    return false;
+  }
+  auto v = FromJavaDoubleArray(env, value, time);
+  if (!v) return false;
+  if (force) {
+    nt::SetEntryTypeValue(entry, v);
+    return JNI_TRUE;
+  }
+  return nt::SetEntryValue(entry, v);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setStringArray
+ * Signature: (IJ[Ljava/lang/Object;Z)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setStringArray
+  (JNIEnv* env, jclass, jint entry, jlong time, jobjectArray value,
+   jboolean force)
+{
+  if (!value) {
+    nullPointerEx.Throw(env, "value cannot be null");
+    return false;
+  }
+  auto v = FromJavaStringArray(env, value, time);
+  if (!v) return false;
+  if (force) {
+    nt::SetEntryTypeValue(entry, v);
+    return JNI_TRUE;
+  }
+  return nt::SetEntryValue(entry, v);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getValue
+ * Signature: (I)Ljava/lang/Object;
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getValue
+  (JNIEnv* env, jclass, jint entry)
+{
+  auto val = nt::GetEntryValue(entry);
+  return MakeJValue(env, val.get());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getBoolean
+ * Signature: (IZ)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getBoolean
+  (JNIEnv*, jclass, jint entry, jboolean defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val->IsBoolean()) return defaultValue;
+  return val->GetBoolean();
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getDouble
+ * Signature: (ID)D
+ */
+JNIEXPORT jdouble JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getDouble
+  (JNIEnv*, jclass, jint entry, jdouble defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val->IsDouble()) return defaultValue;
+  return val->GetDouble();
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getString
+ * Signature: (ILjava/lang/String;)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getString
+  (JNIEnv* env, jclass, jint entry, jstring defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val->IsString()) return defaultValue;
+  return MakeJString(env, val->GetString());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getRaw
+ * Signature: (I[B)[B
+ */
+JNIEXPORT jbyteArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getRaw
+  (JNIEnv* env, jclass, jint entry, jbyteArray defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val->IsRaw()) return defaultValue;
+  return MakeJByteArray(env, val->GetRaw());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getBooleanArray
+ * Signature: (I[Z)[Z
+ */
+JNIEXPORT jbooleanArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getBooleanArray
+  (JNIEnv* env, jclass, jint entry, jbooleanArray defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val->IsBooleanArray()) return defaultValue;
+  return MakeJBooleanArray(env, val->GetBooleanArray());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getDoubleArray
+ * Signature: (I[D)[D
+ */
+JNIEXPORT jdoubleArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getDoubleArray
+  (JNIEnv* env, jclass, jint entry, jdoubleArray defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val->IsDoubleArray()) return defaultValue;
+  return MakeJDoubleArray(env, val->GetDoubleArray());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getStringArray
+ * Signature: (I[Ljava/lang/Object;)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getStringArray
+  (JNIEnv* env, jclass, jint entry, jobjectArray defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val->IsStringArray()) return defaultValue;
+  return MakeJStringArray(env, val->GetStringArray());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultBoolean
+ * Signature: (IJZ)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultBoolean
+  (JNIEnv*, jclass, jint entry, jlong time, jboolean defaultValue)
+{
+  return nt::SetDefaultEntryValue(
+      entry, nt::Value::MakeBoolean(defaultValue != JNI_FALSE, time));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultDouble
+ * Signature: (IJD)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultDouble
+  (JNIEnv*, jclass, jint entry, jlong time, jdouble defaultValue)
+{
+  return nt::SetDefaultEntryValue(entry,
+                                  nt::Value::MakeDouble(defaultValue, time));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultString
+ * Signature: (IJLjava/lang/String;)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultString
+  (JNIEnv* env, jclass, jint entry, jlong time, jstring defaultValue)
+{
+  if (!defaultValue) {
+    nullPointerEx.Throw(env, "defaultValue cannot be null");
+    return false;
+  }
+  return nt::SetDefaultEntryValue(
+      entry, nt::Value::MakeString(JStringRef{env, defaultValue}.str(), time));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultRaw
+ * Signature: (IJ[B)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultRaw
+  (JNIEnv* env, jclass, jint entry, jlong time, jbyteArray defaultValue)
+{
+  if (!defaultValue) {
+    nullPointerEx.Throw(env, "defaultValue cannot be null");
+    return false;
+  }
+  auto v = FromJavaRaw(env, defaultValue, time);
+  return nt::SetDefaultEntryValue(entry, v);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultBooleanArray
+ * Signature: (IJ[Z)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultBooleanArray
+  (JNIEnv* env, jclass, jint entry, jlong time, jbooleanArray defaultValue)
+{
+  if (!defaultValue) {
+    nullPointerEx.Throw(env, "defaultValue cannot be null");
+    return false;
+  }
+  auto v = FromJavaBooleanArray(env, defaultValue, time);
+  return nt::SetDefaultEntryValue(entry, v);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultDoubleArray
+ * Signature: (IJ[D)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultDoubleArray
+  (JNIEnv* env, jclass, jint entry, jlong time, jdoubleArray defaultValue)
+{
+  if (!defaultValue) {
+    nullPointerEx.Throw(env, "defaultValue cannot be null");
+    return false;
+  }
+  auto v = FromJavaDoubleArray(env, defaultValue, time);
+  return nt::SetDefaultEntryValue(entry, v);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setDefaultStringArray
+ * Signature: (IJ[Ljava/lang/Object;)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultStringArray
+  (JNIEnv* env, jclass, jint entry, jlong time, jobjectArray defaultValue)
+{
+  if (!defaultValue) {
+    nullPointerEx.Throw(env, "defaultValue cannot be null");
+    return false;
+  }
+  auto v = FromJavaStringArray(env, defaultValue, time);
+  return nt::SetDefaultEntryValue(entry, v);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setEntryFlags
+ * Signature: (II)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setEntryFlags
+  (JNIEnv*, jclass, jint entry, jint flags)
+{
+  nt::SetEntryFlags(entry, flags);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getEntryFlags
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntryFlags
+  (JNIEnv*, jclass, jint entry)
+{
+  return nt::GetEntryFlags(entry);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    deleteEntry
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_deleteEntry
+  (JNIEnv*, jclass, jint entry)
+{
+  nt::DeleteEntry(entry);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    deleteAllEntries
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_deleteAllEntries
+  (JNIEnv*, jclass, jint inst)
+{
+  nt::DeleteAllEntries(inst);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getEntryInfoHandle
+ * Signature: (Ljava/lang/Object;I)Ljava/lang/Object;
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntryInfoHandle
+  (JNIEnv* env, jclass, jobject inst, jint entry)
+{
+  return MakeJObject(env, inst, nt::GetEntryInfo(entry));
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getEntryInfo
+ * Signature: (Ljava/lang/Object;ILjava/lang/String;I)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntryInfo
+  (JNIEnv* env, jclass, jobject instObject, jint inst, jstring prefix,
+   jint types)
+{
+  if (!prefix) {
+    nullPointerEx.Throw(env, "prefix cannot be null");
+    return nullptr;
+  }
+  auto arr = nt::GetEntryInfo(inst, JStringRef{env, prefix}.str(), types);
+  jobjectArray jarr = env->NewObjectArray(arr.size(), entryInfoCls, nullptr);
+  if (!jarr) return nullptr;
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> jelem{env, MakeJObject(env, instObject, arr[i])};
+    env->SetObjectArrayElement(jarr, i, jelem);
+  }
+  return jarr;
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    createEntryListenerPoller
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_createEntryListenerPoller
+  (JNIEnv*, jclass, jint inst)
+{
+  return nt::CreateEntryListenerPoller(inst);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    destroyEntryListenerPoller
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_destroyEntryListenerPoller
+  (JNIEnv*, jclass, jint poller)
+{
+  nt::DestroyEntryListenerPoller(poller);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    addPolledEntryListener
+ * Signature: (ILjava/lang/String;I)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_addPolledEntryListener__ILjava_lang_String_2I
+  (JNIEnv* env, jclass, jint poller, jstring prefix, jint flags)
+{
+  if (!prefix) {
+    nullPointerEx.Throw(env, "prefix cannot be null");
+    return 0;
+  }
+  return nt::AddPolledEntryListener(poller, JStringRef{env, prefix}.str(),
+                                    flags);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    addPolledEntryListener
+ * Signature: (III)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_addPolledEntryListener__III
+  (JNIEnv* env, jclass, jint poller, jint entry, jint flags)
+{
+  return nt::AddPolledEntryListener(poller, entry, flags);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    pollEntryListener
+ * Signature: (Ljava/lang/Object;I)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_pollEntryListener
+  (JNIEnv* env, jclass, jobject inst, jint poller)
+{
+  auto events = nt::PollEntryListener(poller);
+  if (events.empty()) {
+    interruptedEx.Throw(env, "PollEntryListener interrupted");
+    return nullptr;
+  }
+  return MakeJObject(env, inst, events);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    pollEntryListenerTimeout
+ * Signature: (Ljava/lang/Object;ID)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_pollEntryListenerTimeout
+  (JNIEnv* env, jclass, jobject inst, jint poller, jdouble timeout)
+{
+  bool timed_out = false;
+  auto events = nt::PollEntryListener(poller, timeout, &timed_out);
+  if (events.empty() && !timed_out) {
+    interruptedEx.Throw(env, "PollEntryListener interrupted");
+    return nullptr;
+  }
+  return MakeJObject(env, inst, events);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    cancelPollEntryListener
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_cancelPollEntryListener
+  (JNIEnv*, jclass, jint poller)
+{
+  nt::CancelPollEntryListener(poller);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    removeEntryListener
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_removeEntryListener
+  (JNIEnv*, jclass, jint entryListenerUid)
+{
+  nt::RemoveEntryListener(entryListenerUid);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    waitForEntryListenerQueue
+ * Signature: (ID)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_waitForEntryListenerQueue
+  (JNIEnv*, jclass, jint inst, jdouble timeout)
+{
+  return nt::WaitForEntryListenerQueue(inst, timeout);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    createConnectionListenerPoller
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_createConnectionListenerPoller
+  (JNIEnv*, jclass, jint inst)
+{
+  return nt::CreateConnectionListenerPoller(inst);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    destroyConnectionListenerPoller
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_destroyConnectionListenerPoller
+  (JNIEnv*, jclass, jint poller)
+{
+  nt::DestroyConnectionListenerPoller(poller);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    addPolledConnectionListener
+ * Signature: (IZ)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_addPolledConnectionListener
+  (JNIEnv* env, jclass, jint poller, jboolean immediateNotify)
+{
+  return nt::AddPolledConnectionListener(poller, immediateNotify);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    pollConnectionListener
+ * Signature: (Ljava/lang/Object;I)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_pollConnectionListener
+  (JNIEnv* env, jclass, jobject inst, jint poller)
+{
+  auto events = nt::PollConnectionListener(poller);
+  if (events.empty()) {
+    interruptedEx.Throw(env, "PollConnectionListener interrupted");
+    return nullptr;
+  }
+  return MakeJObject(env, inst, events);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    pollConnectionListenerTimeout
+ * Signature: (Ljava/lang/Object;ID)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_pollConnectionListenerTimeout
+  (JNIEnv* env, jclass, jobject inst, jint poller, jdouble timeout)
+{
+  bool timed_out = false;
+  auto events = nt::PollConnectionListener(poller, timeout, &timed_out);
+  if (events.empty() && !timed_out) {
+    interruptedEx.Throw(env, "PollConnectionListener interrupted");
+    return nullptr;
+  }
+  return MakeJObject(env, inst, events);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    cancelPollConnectionListener
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_cancelPollConnectionListener
+  (JNIEnv*, jclass, jint poller)
+{
+  nt::CancelPollConnectionListener(poller);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    removeConnectionListener
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_removeConnectionListener
+  (JNIEnv*, jclass, jint connListenerUid)
+{
+  nt::RemoveConnectionListener(connListenerUid);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    waitForConnectionListenerQueue
+ * Signature: (ID)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_waitForConnectionListenerQueue
+  (JNIEnv*, jclass, jint inst, jdouble timeout)
+{
+  return nt::WaitForConnectionListenerQueue(inst, timeout);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    createRpcCallPoller
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_createRpcCallPoller
+  (JNIEnv*, jclass, jint inst)
+{
+  return nt::CreateRpcCallPoller(inst);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    destroyRpcCallPoller
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_destroyRpcCallPoller
+  (JNIEnv*, jclass, jint poller)
+{
+  nt::DestroyRpcCallPoller(poller);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    createPolledRpc
+ * Signature: (I[BI)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_createPolledRpc
+  (JNIEnv* env, jclass, jint entry, jbyteArray def, jint poller)
+{
+  if (!def) {
+    nullPointerEx.Throw(env, "def cannot be null");
+    return;
+  }
+  nt::CreatePolledRpc(entry, JByteArrayRef{env, def}, poller);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    pollRpc
+ * Signature: (Ljava/lang/Object;I)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_pollRpc
+  (JNIEnv* env, jclass, jobject inst, jint poller)
+{
+  auto infos = nt::PollRpc(poller);
+  if (infos.empty()) {
+    interruptedEx.Throw(env, "PollRpc interrupted");
+    return nullptr;
+  }
+  return MakeJObject(env, inst, infos);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    pollRpcTimeout
+ * Signature: (Ljava/lang/Object;ID)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_pollRpcTimeout
+  (JNIEnv* env, jclass, jobject inst, jint poller, jdouble timeout)
+{
+  bool timed_out = false;
+  auto infos = nt::PollRpc(poller, timeout, &timed_out);
+  if (infos.empty() && !timed_out) {
+    interruptedEx.Throw(env, "PollRpc interrupted");
+    return nullptr;
+  }
+  return MakeJObject(env, inst, infos);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    cancelPollRpc
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_cancelPollRpc
+  (JNIEnv*, jclass, jint poller)
+{
+  nt::CancelPollRpc(poller);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    waitForRpcCallQueue
+ * Signature: (ID)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_waitForRpcCallQueue
+  (JNIEnv*, jclass, jint inst, jdouble timeout)
+{
+  return nt::WaitForRpcCallQueue(inst, timeout);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    postRpcResponse
+ * Signature: (II[B)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_postRpcResponse
+  (JNIEnv* env, jclass, jint entry, jint call, jbyteArray result)
+{
+  if (!result) {
+    nullPointerEx.Throw(env, "result cannot be null");
+    return false;
+  }
+  return nt::PostRpcResponse(entry, call, JByteArrayRef{env, result});
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    callRpc
+ * Signature: (I[B)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_callRpc
+  (JNIEnv* env, jclass, jint entry, jbyteArray params)
+{
+  if (!params) {
+    nullPointerEx.Throw(env, "params cannot be null");
+    return 0;
+  }
+  return nt::CallRpc(entry, JByteArrayRef{env, params});
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getRpcResult
+ * Signature: (II)[B
+ */
+JNIEXPORT jbyteArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getRpcResult__II
+  (JNIEnv* env, jclass, jint entry, jint call)
+{
+  std::string result;
+  if (!nt::GetRpcResult(entry, call, &result)) return nullptr;
+  return MakeJByteArray(env, result);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getRpcResult
+ * Signature: (IID)[B
+ */
+JNIEXPORT jbyteArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getRpcResult__IID
+  (JNIEnv* env, jclass, jint entry, jint call, jdouble timeout)
+{
+  std::string result;
+  bool timed_out = false;
+  if (!nt::GetRpcResult(entry, call, &result, timeout, &timed_out))
+    return nullptr;
+  return MakeJByteArray(env, result);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    cancelRpcResult
+ * Signature: (II)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_cancelRpcResult
+  (JNIEnv*, jclass, jint entry, jint call)
+{
+  nt::CancelRpcResult(entry, call);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getRpc
+ * Signature: (I[B)[B
+ */
+JNIEXPORT jbyteArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getRpc
+  (JNIEnv* env, jclass, jint entry, jbyteArray defaultValue)
+{
+  auto val = nt::GetEntryValue(entry);
+  if (!val || !val->IsRpc()) return defaultValue;
+  return MakeJByteArray(env, val->GetRpc());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setNetworkIdentity
+ * Signature: (ILjava/lang/String;)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setNetworkIdentity
+  (JNIEnv* env, jclass, jint inst, jstring name)
+{
+  if (!name) {
+    nullPointerEx.Throw(env, "name cannot be null");
+    return;
+  }
+  nt::SetNetworkIdentity(inst, JStringRef{env, name}.str());
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getNetworkMode
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getNetworkMode
+  (JNIEnv*, jclass, jint inst)
+{
+  return nt::GetNetworkMode(inst);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    startServer
+ * Signature: (ILjava/lang/String;Ljava/lang/String;I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_startServer
+  (JNIEnv* env, jclass, jint inst, jstring persistFilename,
+   jstring listenAddress, jint port)
+{
+  if (!persistFilename) {
+    nullPointerEx.Throw(env, "persistFilename cannot be null");
+    return;
+  }
+  if (!listenAddress) {
+    nullPointerEx.Throw(env, "listenAddress cannot be null");
+    return;
+  }
+  nt::StartServer(inst, JStringRef{env, persistFilename}.str(),
+                  JStringRef{env, listenAddress}.c_str(), port);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    stopServer
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_stopServer
+  (JNIEnv*, jclass, jint inst)
+{
+  nt::StopServer(inst);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    startClient
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_startClient__I
+  (JNIEnv*, jclass, jint inst)
+{
+  nt::StartClient(inst);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    startClient
+ * Signature: (ILjava/lang/String;I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_startClient__ILjava_lang_String_2I
+  (JNIEnv* env, jclass, jint inst, jstring serverName, jint port)
+{
+  if (!serverName) {
+    nullPointerEx.Throw(env, "serverName cannot be null");
+    return;
+  }
+  nt::StartClient(inst, JStringRef{env, serverName}.c_str(), port);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    startClient
+ * Signature: (I[Ljava/lang/Object;[I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_startClient__I_3Ljava_lang_String_2_3I
+  (JNIEnv* env, jclass, jint inst, jobjectArray serverNames, jintArray ports)
+{
+  if (!serverNames) {
+    nullPointerEx.Throw(env, "serverNames cannot be null");
+    return;
+  }
+  if (!ports) {
+    nullPointerEx.Throw(env, "ports cannot be null");
+    return;
+  }
+  int len = env->GetArrayLength(serverNames);
+  if (len != env->GetArrayLength(ports)) {
+    illegalArgEx.Throw(env,
+                       "serverNames and ports arrays must be the same size");
+    return;
+  }
+  jint* portInts = env->GetIntArrayElements(ports, nullptr);
+  if (!portInts) return;
+
+  std::vector<std::string> names;
+  std::vector<std::pair<nt::StringRef, unsigned int>> servers;
+  names.reserve(len);
+  servers.reserve(len);
+  for (int i = 0; i < len; ++i) {
+    JLocal<jstring> elem{
+        env, static_cast<jstring>(env->GetObjectArrayElement(serverNames, i))};
+    if (!elem) {
+      nullPointerEx.Throw(env, "null string in serverNames");
+      return;
+    }
+    names.emplace_back(JStringRef{env, elem}.str());
+    servers.emplace_back(
+        std::make_pair(nt::StringRef(names.back()), portInts[i]));
+  }
+  env->ReleaseIntArrayElements(ports, portInts, JNI_ABORT);
+  nt::StartClient(inst, servers);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    startClientTeam
+ * Signature: (III)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_startClientTeam
+  (JNIEnv* env, jclass cls, jint inst, jint team, jint port)
+{
+  nt::StartClientTeam(inst, team, port);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    stopClient
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_stopClient
+  (JNIEnv*, jclass, jint inst)
+{
+  nt::StopClient(inst);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setServer
+ * Signature: (ILjava/lang/String;I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setServer__ILjava_lang_String_2I
+  (JNIEnv* env, jclass, jint inst, jstring serverName, jint port)
+{
+  if (!serverName) {
+    nullPointerEx.Throw(env, "serverName cannot be null");
+    return;
+  }
+  nt::SetServer(inst, JStringRef{env, serverName}.c_str(), port);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setServer
+ * Signature: (I[Ljava/lang/Object;[I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setServer__I_3Ljava_lang_String_2_3I
+  (JNIEnv* env, jclass, jint inst, jobjectArray serverNames, jintArray ports)
+{
+  if (!serverNames) {
+    nullPointerEx.Throw(env, "serverNames cannot be null");
+    return;
+  }
+  if (!ports) {
+    nullPointerEx.Throw(env, "ports cannot be null");
+    return;
+  }
+  int len = env->GetArrayLength(serverNames);
+  if (len != env->GetArrayLength(ports)) {
+    illegalArgEx.Throw(env,
+                       "serverNames and ports arrays must be the same size");
+    return;
+  }
+  jint* portInts = env->GetIntArrayElements(ports, nullptr);
+  if (!portInts) return;
+
+  std::vector<std::string> names;
+  std::vector<std::pair<nt::StringRef, unsigned int>> servers;
+  names.reserve(len);
+  servers.reserve(len);
+  for (int i = 0; i < len; ++i) {
+    JLocal<jstring> elem{
+        env, static_cast<jstring>(env->GetObjectArrayElement(serverNames, i))};
+    if (!elem) {
+      nullPointerEx.Throw(env, "null string in serverNames");
+      return;
+    }
+    names.emplace_back(JStringRef{env, elem}.str());
+    servers.emplace_back(
+        std::make_pair(nt::StringRef(names.back()), portInts[i]));
+  }
+  env->ReleaseIntArrayElements(ports, portInts, JNI_ABORT);
+  nt::SetServer(inst, servers);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setServerTeam
+ * Signature: (III)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setServerTeam
+  (JNIEnv* env, jclass, jint inst, jint team, jint port)
+{
+  nt::SetServerTeam(inst, team, port);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    startDSClient
+ * Signature: (II)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_startDSClient
+  (JNIEnv*, jclass, jint inst, jint port)
+{
+  nt::StartDSClient(inst, port);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    stopDSClient
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_stopDSClient
+  (JNIEnv*, jclass, jint inst)
+{
+  nt::StopDSClient(inst);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    setUpdateRate
+ * Signature: (ID)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setUpdateRate
+  (JNIEnv*, jclass, jint inst, jdouble interval)
+{
+  nt::SetUpdateRate(inst, interval);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    flush
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_flush
+  (JNIEnv*, jclass, jint inst)
+{
+  nt::Flush(inst);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    getConnections
+ * Signature: (I)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getConnections
+  (JNIEnv* env, jclass, jint inst)
+{
+  auto arr = nt::GetConnections(inst);
+  jobjectArray jarr =
+      env->NewObjectArray(arr.size(), connectionInfoCls, nullptr);
+  if (!jarr) return nullptr;
+  for (size_t i = 0; i < arr.size(); ++i) {
+    JLocal<jobject> jelem{env, MakeJObject(env, arr[i])};
+    env->SetObjectArrayElement(jarr, i, jelem);
+  }
+  return jarr;
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    isConnected
+ * Signature: (I)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_isConnected
+  (JNIEnv*, jclass, jint inst)
+{
+  return nt::IsConnected(inst);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    savePersistent
+ * Signature: (ILjava/lang/String;)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_savePersistent
+  (JNIEnv* env, jclass, jint inst, jstring filename)
+{
+  if (!filename) {
+    nullPointerEx.Throw(env, "filename cannot be null");
+    return;
+  }
+  const char* err = nt::SavePersistent(inst, JStringRef{env, filename}.str());
+  if (err) persistentEx.Throw(env, err);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    loadPersistent
+ * Signature: (ILjava/lang/String;)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_loadPersistent
+  (JNIEnv* env, jclass, jint inst, jstring filename)
+{
+  if (!filename) {
+    nullPointerEx.Throw(env, "filename cannot be null");
+    return nullptr;
+  }
+  std::vector<std::string> warns;
+  const char* err = nt::LoadPersistent(inst, JStringRef{env, filename}.str(),
+                                       [&](size_t line, const char* msg) {
+                                         wpi::SmallString<128> warn;
+                                         wpi::raw_svector_ostream oss(warn);
+                                         oss << line << ": " << msg;
+                                         warns.emplace_back(oss.str());
+                                       });
+  if (err) {
+    persistentEx.Throw(env, err);
+    return nullptr;
+  }
+  return MakeJStringArray(env, warns);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    saveEntries
+ * Signature: (ILjava/lang/String;Ljava/lang/String;)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_saveEntries
+  (JNIEnv* env, jclass, jint inst, jstring filename, jstring prefix)
+{
+  if (!filename) {
+    nullPointerEx.Throw(env, "filename cannot be null");
+    return;
+  }
+  if (!prefix) {
+    nullPointerEx.Throw(env, "prefix cannot be null");
+    return;
+  }
+  const char* err = nt::SaveEntries(inst, JStringRef{env, filename}.str(),
+                                    JStringRef{env, prefix}.str());
+  if (err) persistentEx.Throw(env, err);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    loadEntries
+ * Signature: (ILjava/lang/String;Ljava/lang/String;)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_loadEntries
+  (JNIEnv* env, jclass, jint inst, jstring filename, jstring prefix)
+{
+  if (!filename) {
+    nullPointerEx.Throw(env, "filename cannot be null");
+    return nullptr;
+  }
+  if (!prefix) {
+    nullPointerEx.Throw(env, "prefix cannot be null");
+    return nullptr;
+  }
+  std::vector<std::string> warns;
+  const char* err = nt::LoadEntries(inst, JStringRef{env, filename}.str(),
+                                    JStringRef{env, prefix}.str(),
+                                    [&](size_t line, const char* msg) {
+                                      wpi::SmallString<128> warn;
+                                      wpi::raw_svector_ostream oss(warn);
+                                      oss << line << ": " << msg;
+                                      warns.emplace_back(oss.str());
+                                    });
+  if (err) {
+    persistentEx.Throw(env, err);
+    return nullptr;
+  }
+  return MakeJStringArray(env, warns);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    now
+ * Signature: ()J
+ */
+JNIEXPORT jlong JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_now
+  (JNIEnv*, jclass)
+{
+  return nt::Now();
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    createLoggerPoller
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_createLoggerPoller
+  (JNIEnv*, jclass, jint inst)
+{
+  return nt::CreateLoggerPoller(inst);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    destroyLoggerPoller
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_destroyLoggerPoller
+  (JNIEnv*, jclass, jint poller)
+{
+  nt::DestroyLoggerPoller(poller);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    addPolledLogger
+ * Signature: (III)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_addPolledLogger
+  (JNIEnv*, jclass, jint poller, jint minLevel, jint maxLevel)
+{
+  return nt::AddPolledLogger(poller, minLevel, maxLevel);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    pollLogger
+ * Signature: (Ljava/lang/Object;I)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_pollLogger
+  (JNIEnv* env, jclass, jobject inst, jint poller)
+{
+  auto events = nt::PollLogger(poller);
+  if (events.empty()) {
+    interruptedEx.Throw(env, "PollLogger interrupted");
+    return nullptr;
+  }
+  return MakeJObject(env, inst, events);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    pollLoggerTimeout
+ * Signature: (Ljava/lang/Object;ID)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_pollLoggerTimeout
+  (JNIEnv* env, jclass, jobject inst, jint poller, jdouble timeout)
+{
+  bool timed_out = false;
+  auto events = nt::PollLogger(poller, timeout, &timed_out);
+  if (events.empty() && !timed_out) {
+    interruptedEx.Throw(env, "PollLogger interrupted");
+    return nullptr;
+  }
+  return MakeJObject(env, inst, events);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    cancelPollLogger
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_cancelPollLogger
+  (JNIEnv*, jclass, jint poller)
+{
+  nt::CancelPollLogger(poller);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    removeLogger
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_removeLogger
+  (JNIEnv*, jclass, jint logger)
+{
+  nt::RemoveLogger(logger);
+}
+
+/*
+ * Class:     edu_wpi_first_networktables_NetworkTablesJNI
+ * Method:    waitForLoggerQueue
+ * Signature: (ID)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_waitForLoggerQueue
+  (JNIEnv*, jclass, jint inst, jdouble timeout)
+{
+  return nt::WaitForLoggerQueue(inst, timeout);
+}
+
+}  // extern "C"
diff --git a/ntcore/src/main/native/cpp/networktables/NetworkTable.cpp b/ntcore/src/main/native/cpp/networktables/NetworkTable.cpp
new file mode 100644
index 0000000..1ec0942
--- /dev/null
+++ b/ntcore/src/main/native/cpp/networktables/NetworkTable.cpp
@@ -0,0 +1,562 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "networktables/NetworkTable.h"
+
+#include <algorithm>
+
+#include <wpi/SmallString.h>
+#include <wpi/StringMap.h>
+#include <wpi/raw_ostream.h>
+
+#include "networktables/NetworkTableInstance.h"
+#include "ntcore.h"
+#include "tables/ITableListener.h"
+
+using namespace nt;
+
+#ifdef __GNUC__
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
+const char NetworkTable::PATH_SEPARATOR_CHAR = '/';
+std::string NetworkTable::s_persistent_filename = "networktables.ini";
+bool NetworkTable::s_client = false;
+bool NetworkTable::s_enable_ds = true;
+bool NetworkTable::s_running = false;
+unsigned int NetworkTable::s_port = NT_DEFAULT_PORT;
+
+StringRef NetworkTable::BasenameKey(StringRef key) {
+  size_t slash = key.rfind(PATH_SEPARATOR_CHAR);
+  if (slash == StringRef::npos) return key;
+  return key.substr(slash + 1);
+}
+
+std::string NetworkTable::NormalizeKey(const Twine& key,
+                                       bool withLeadingSlash) {
+  wpi::SmallString<128> buf;
+  return NormalizeKey(key, buf, withLeadingSlash);
+}
+
+StringRef NetworkTable::NormalizeKey(const Twine& key,
+                                     wpi::SmallVectorImpl<char>& buf,
+                                     bool withLeadingSlash) {
+  buf.clear();
+  if (withLeadingSlash) buf.push_back(PATH_SEPARATOR_CHAR);
+  // for each path element, add it with a slash following
+  wpi::SmallString<128> keyBuf;
+  StringRef keyStr = key.toStringRef(keyBuf);
+  wpi::SmallVector<StringRef, 16> parts;
+  keyStr.split(parts, PATH_SEPARATOR_CHAR, -1, false);
+  for (auto i = parts.begin(); i != parts.end(); ++i) {
+    buf.append(i->begin(), i->end());
+    buf.push_back(PATH_SEPARATOR_CHAR);
+  }
+  // remove trailing slash if the input key didn't have one
+  if (!keyStr.empty() && keyStr.back() != PATH_SEPARATOR_CHAR) buf.pop_back();
+  return StringRef(buf.data(), buf.size());
+}
+
+std::vector<std::string> NetworkTable::GetHierarchy(const Twine& key) {
+  std::vector<std::string> hierarchy;
+  hierarchy.emplace_back(1, PATH_SEPARATOR_CHAR);
+  // for each path element, add it to the end of what we built previously
+  wpi::SmallString<128> keyBuf;
+  StringRef keyStr = key.toStringRef(keyBuf);
+  wpi::SmallString<128> path;
+  wpi::SmallVector<StringRef, 16> parts;
+  keyStr.split(parts, PATH_SEPARATOR_CHAR, -1, false);
+  if (!parts.empty()) {
+    for (auto i = parts.begin(); i != parts.end(); ++i) {
+      path += PATH_SEPARATOR_CHAR;
+      path += *i;
+      hierarchy.emplace_back(path.str());
+    }
+    // handle trailing slash
+    if (keyStr.back() == PATH_SEPARATOR_CHAR) {
+      path += PATH_SEPARATOR_CHAR;
+      hierarchy.emplace_back(path.str());
+    }
+  }
+  return hierarchy;
+}
+
+void NetworkTable::Initialize() {
+  if (s_running) Shutdown();
+  auto inst = NetworkTableInstance::GetDefault();
+  if (s_client) {
+    inst.StartClient();
+    if (s_enable_ds) inst.StartDSClient(s_port);
+  } else {
+    inst.StartServer(s_persistent_filename, "", s_port);
+  }
+  s_running = true;
+}
+
+void NetworkTable::Shutdown() {
+  if (!s_running) return;
+  auto inst = NetworkTableInstance::GetDefault();
+  if (s_client) {
+    inst.StopDSClient();
+    inst.StopClient();
+  } else {
+    inst.StopServer();
+  }
+  s_running = false;
+}
+
+void NetworkTable::SetClientMode() { s_client = true; }
+
+void NetworkTable::SetServerMode() { s_client = false; }
+
+void NetworkTable::SetTeam(int team) {
+  auto inst = NetworkTableInstance::GetDefault();
+  inst.SetServerTeam(team, s_port);
+  if (s_enable_ds) inst.StartDSClient(s_port);
+}
+
+void NetworkTable::SetIPAddress(StringRef address) {
+  auto inst = NetworkTableInstance::GetDefault();
+  wpi::SmallString<32> addr_copy{address};
+  inst.SetServer(addr_copy.c_str(), s_port);
+
+  // Stop the DS client if we're explicitly connecting to localhost
+  if (address == "localhost" || address == "127.0.0.1")
+    inst.StopDSClient();
+  else if (s_enable_ds)
+    inst.StartDSClient(s_port);
+}
+
+void NetworkTable::SetIPAddress(ArrayRef<std::string> addresses) {
+  auto inst = NetworkTableInstance::GetDefault();
+  wpi::SmallVector<StringRef, 8> servers;
+  for (const auto& ip_address : addresses) servers.emplace_back(ip_address);
+  inst.SetServer(servers, s_port);
+
+  // Stop the DS client if we're explicitly connecting to localhost
+  if (!addresses.empty() &&
+      (addresses[0] == "localhost" || addresses[0] == "127.0.0.1"))
+    inst.StopDSClient();
+  else if (s_enable_ds)
+    inst.StartDSClient(s_port);
+}
+
+void NetworkTable::SetPort(unsigned int port) { s_port = port; }
+
+void NetworkTable::SetDSClientEnabled(bool enabled) {
+  auto inst = NetworkTableInstance::GetDefault();
+  s_enable_ds = enabled;
+  if (s_enable_ds)
+    inst.StartDSClient(s_port);
+  else
+    inst.StopDSClient();
+}
+
+void NetworkTable::SetPersistentFilename(StringRef filename) {
+  s_persistent_filename = filename;
+}
+
+void NetworkTable::SetNetworkIdentity(StringRef name) {
+  NetworkTableInstance::GetDefault().SetNetworkIdentity(name);
+}
+
+void NetworkTable::GlobalDeleteAll() {
+  NetworkTableInstance::GetDefault().DeleteAllEntries();
+}
+
+void NetworkTable::Flush() { NetworkTableInstance::GetDefault().Flush(); }
+
+void NetworkTable::SetUpdateRate(double interval) {
+  NetworkTableInstance::GetDefault().SetUpdateRate(interval);
+}
+
+const char* NetworkTable::SavePersistent(StringRef filename) {
+  return NetworkTableInstance::GetDefault().SavePersistent(filename);
+}
+
+const char* NetworkTable::LoadPersistent(
+    StringRef filename,
+    std::function<void(size_t line, const char* msg)> warn) {
+  return NetworkTableInstance::GetDefault().LoadPersistent(filename, warn);
+}
+
+std::shared_ptr<NetworkTable> NetworkTable::GetTable(StringRef key) {
+  if (!s_running) Initialize();
+  return NetworkTableInstance::GetDefault().GetTable(key);
+}
+
+NetworkTable::NetworkTable(NT_Inst inst, const Twine& path, const private_init&)
+    : m_inst(inst), m_path(path.str()) {}
+
+NetworkTable::~NetworkTable() {
+  for (auto& i : m_listeners) RemoveEntryListener(i.second);
+  for (auto i : m_lambdaListeners) RemoveEntryListener(i);
+}
+
+NetworkTableInstance NetworkTable::GetInstance() const {
+  return NetworkTableInstance{m_inst};
+}
+
+NetworkTableEntry NetworkTable::GetEntry(const Twine& key) const {
+  wpi::SmallString<128> keyBuf;
+  StringRef keyStr = key.toStringRef(keyBuf);
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  NT_Entry& entry = m_entries[keyStr];
+  if (entry == 0) {
+    entry = nt::GetEntry(m_inst, m_path + Twine(PATH_SEPARATOR_CHAR) + keyStr);
+  }
+  return NetworkTableEntry{entry};
+}
+
+NT_EntryListener NetworkTable::AddEntryListener(TableEntryListener listener,
+                                                unsigned int flags) const {
+  size_t prefix_len = m_path.size() + 1;
+  return nt::AddEntryListener(
+      m_inst, m_path + Twine(PATH_SEPARATOR_CHAR),
+      [=](const EntryNotification& event) {
+        StringRef relative_key = event.name.substr(prefix_len);
+        if (relative_key.find(PATH_SEPARATOR_CHAR) != StringRef::npos) return;
+        listener(const_cast<NetworkTable*>(this), relative_key,
+                 NetworkTableEntry{event.entry}, event.value, event.flags);
+      },
+      flags);
+}
+
+NT_EntryListener NetworkTable::AddEntryListener(const Twine& key,
+                                                TableEntryListener listener,
+                                                unsigned int flags) const {
+  size_t prefix_len = m_path.size() + 1;
+  auto entry = GetEntry(key);
+  return nt::AddEntryListener(entry.GetHandle(),
+                              [=](const EntryNotification& event) {
+                                listener(const_cast<NetworkTable*>(this),
+                                         event.name.substr(prefix_len), entry,
+                                         event.value, event.flags);
+                              },
+                              flags);
+}
+
+void NetworkTable::RemoveEntryListener(NT_EntryListener listener) const {
+  nt::RemoveEntryListener(listener);
+}
+
+void NetworkTable::AddTableListener(ITableListener* listener) {
+  AddTableListenerEx(listener, NT_NOTIFY_NEW | NT_NOTIFY_UPDATE);
+}
+
+void NetworkTable::AddTableListener(ITableListener* listener,
+                                    bool immediateNotify) {
+  unsigned int flags = NT_NOTIFY_NEW | NT_NOTIFY_UPDATE;
+  if (immediateNotify) flags |= NT_NOTIFY_IMMEDIATE;
+  AddTableListenerEx(listener, flags);
+}
+
+void NetworkTable::AddTableListenerEx(ITableListener* listener,
+                                      unsigned int flags) {
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  wpi::SmallString<128> path(m_path);
+  path += PATH_SEPARATOR_CHAR;
+  size_t prefix_len = path.size();
+  NT_EntryListener id = nt::AddEntryListener(
+      m_inst, path,
+      [=](const EntryNotification& event) {
+        StringRef relative_key = event.name.substr(prefix_len);
+        if (relative_key.find(PATH_SEPARATOR_CHAR) != StringRef::npos) return;
+        listener->ValueChangedEx(this, relative_key, event.value, event.flags);
+      },
+      flags);
+  m_listeners.emplace_back(listener, id);
+}
+
+void NetworkTable::AddTableListener(StringRef key, ITableListener* listener,
+                                    bool immediateNotify) {
+  unsigned int flags = NT_NOTIFY_NEW | NT_NOTIFY_UPDATE;
+  if (immediateNotify) flags |= NT_NOTIFY_IMMEDIATE;
+  AddTableListenerEx(key, listener, flags);
+}
+
+void NetworkTable::AddTableListenerEx(StringRef key, ITableListener* listener,
+                                      unsigned int flags) {
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  size_t prefix_len = m_path.size() + 1;
+  auto entry = GetEntry(key);
+  NT_EntryListener id = nt::AddEntryListener(
+      entry.GetHandle(),
+      [=](const EntryNotification& event) {
+        listener->ValueChangedEx(this, event.name.substr(prefix_len),
+                                 event.value, event.flags);
+      },
+      flags);
+  m_listeners.emplace_back(listener, id);
+}
+
+void NetworkTable::AddSubTableListener(ITableListener* listener) {
+  AddSubTableListener(listener, false);
+}
+
+NT_EntryListener NetworkTable::AddSubTableListener(TableListener listener,
+                                                   bool localNotify) {
+  size_t prefix_len = m_path.size() + 1;
+
+  // The lambda needs to be copyable, but StringMap is not, so use
+  // a shared_ptr to it.
+  auto notified_tables = std::make_shared<wpi::StringMap<char>>();
+
+  unsigned int flags = NT_NOTIFY_NEW | NT_NOTIFY_IMMEDIATE;
+  if (localNotify) flags |= NT_NOTIFY_LOCAL;
+  NT_EntryListener id = nt::AddEntryListener(
+      m_inst, m_path + Twine(PATH_SEPARATOR_CHAR),
+      [=](const EntryNotification& event) {
+        StringRef relative_key = event.name.substr(prefix_len);
+        auto end_sub_table = relative_key.find(PATH_SEPARATOR_CHAR);
+        if (end_sub_table == StringRef::npos) return;
+        StringRef sub_table_key = relative_key.substr(0, end_sub_table);
+        if (notified_tables->find(sub_table_key) == notified_tables->end())
+          return;
+        notified_tables->insert(std::make_pair(sub_table_key, '\0'));
+        listener(this, sub_table_key, this->GetSubTable(sub_table_key));
+      },
+      flags);
+  m_lambdaListeners.emplace_back(id);
+  return id;
+}
+
+void NetworkTable::RemoveTableListener(NT_EntryListener listener) {
+  nt::RemoveEntryListener(listener);
+  auto matches_begin =
+      std::remove(m_lambdaListeners.begin(), m_lambdaListeners.end(), listener);
+  m_lambdaListeners.erase(matches_begin, m_lambdaListeners.end());
+}
+
+void NetworkTable::AddSubTableListener(ITableListener* listener,
+                                       bool localNotify) {
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  size_t prefix_len = m_path.size() + 1;
+
+  // The lambda needs to be copyable, but StringMap is not, so use
+  // a shared_ptr to it.
+  auto notified_tables = std::make_shared<wpi::StringMap<char>>();
+
+  unsigned int flags = NT_NOTIFY_NEW | NT_NOTIFY_IMMEDIATE;
+  if (localNotify) flags |= NT_NOTIFY_LOCAL;
+  NT_EntryListener id = nt::AddEntryListener(
+      m_inst, m_path + Twine(PATH_SEPARATOR_CHAR),
+      [=](const EntryNotification& event) {
+        StringRef relative_key = event.name.substr(prefix_len);
+        auto end_sub_table = relative_key.find(PATH_SEPARATOR_CHAR);
+        if (end_sub_table == StringRef::npos) return;
+        StringRef sub_table_key = relative_key.substr(0, end_sub_table);
+        if (notified_tables->find(sub_table_key) == notified_tables->end())
+          return;
+        notified_tables->insert(std::make_pair(sub_table_key, '\0'));
+        listener->ValueChangedEx(this, sub_table_key, nullptr, event.flags);
+      },
+      flags);
+  m_listeners.emplace_back(listener, id);
+}
+
+void NetworkTable::RemoveTableListener(ITableListener* listener) {
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  auto matches_begin =
+      std::remove_if(m_listeners.begin(), m_listeners.end(),
+                     [=](const Listener& x) { return x.first == listener; });
+
+  for (auto i = matches_begin; i != m_listeners.end(); ++i)
+    RemoveEntryListener(i->second);
+  m_listeners.erase(matches_begin, m_listeners.end());
+}
+
+std::shared_ptr<NetworkTable> NetworkTable::GetSubTable(
+    const Twine& key) const {
+  return std::make_shared<NetworkTable>(
+      m_inst, m_path + Twine(PATH_SEPARATOR_CHAR) + key, private_init{});
+}
+
+bool NetworkTable::ContainsKey(const Twine& key) const {
+  if (key.isTriviallyEmpty() ||
+      (key.isSingleStringRef() && key.getSingleStringRef().empty()))
+    return false;
+  return GetEntry(key).Exists();
+}
+
+bool NetworkTable::ContainsSubTable(const Twine& key) const {
+  return !GetEntryInfo(m_inst,
+                       m_path + Twine(PATH_SEPARATOR_CHAR) + key +
+                           Twine(PATH_SEPARATOR_CHAR),
+                       0)
+              .empty();
+}
+
+std::vector<std::string> NetworkTable::GetKeys(int types) const {
+  std::vector<std::string> keys;
+  size_t prefix_len = m_path.size() + 1;
+  auto infos = GetEntryInfo(m_inst, m_path + Twine(PATH_SEPARATOR_CHAR), types);
+  std::lock_guard<wpi::mutex> lock(m_mutex);
+  for (auto& info : infos) {
+    auto relative_key = StringRef(info.name).substr(prefix_len);
+    if (relative_key.find(PATH_SEPARATOR_CHAR) != StringRef::npos) continue;
+    keys.push_back(relative_key);
+    m_entries[relative_key] = info.entry;
+  }
+  return keys;
+}
+
+std::vector<std::string> NetworkTable::GetSubTables() const {
+  std::vector<std::string> keys;
+  size_t prefix_len = m_path.size() + 1;
+  for (auto& entry :
+       GetEntryInfo(m_inst, m_path + Twine(PATH_SEPARATOR_CHAR), 0)) {
+    auto relative_key = StringRef(entry.name).substr(prefix_len);
+    size_t end_subtable = relative_key.find(PATH_SEPARATOR_CHAR);
+    if (end_subtable == StringRef::npos) continue;
+    keys.push_back(relative_key.substr(0, end_subtable));
+  }
+  return keys;
+}
+
+void NetworkTable::SetPersistent(StringRef key) {
+  GetEntry(key).SetPersistent();
+}
+
+void NetworkTable::ClearPersistent(StringRef key) {
+  GetEntry(key).ClearPersistent();
+}
+
+bool NetworkTable::IsPersistent(StringRef key) const {
+  return GetEntry(key).IsPersistent();
+}
+
+void NetworkTable::SetFlags(StringRef key, unsigned int flags) {
+  GetEntry(key).SetFlags(flags);
+}
+
+void NetworkTable::ClearFlags(StringRef key, unsigned int flags) {
+  GetEntry(key).ClearFlags(flags);
+}
+
+unsigned int NetworkTable::GetFlags(StringRef key) const {
+  return GetEntry(key).GetFlags();
+}
+
+void NetworkTable::Delete(const Twine& key) { GetEntry(key).Delete(); }
+
+bool NetworkTable::PutNumber(StringRef key, double value) {
+  return GetEntry(key).SetDouble(value);
+}
+
+bool NetworkTable::SetDefaultNumber(StringRef key, double defaultValue) {
+  return GetEntry(key).SetDefaultDouble(defaultValue);
+}
+
+double NetworkTable::GetNumber(StringRef key, double defaultValue) const {
+  return GetEntry(key).GetDouble(defaultValue);
+}
+
+bool NetworkTable::PutString(StringRef key, StringRef value) {
+  return GetEntry(key).SetString(value);
+}
+
+bool NetworkTable::SetDefaultString(StringRef key, StringRef defaultValue) {
+  return GetEntry(key).SetDefaultString(defaultValue);
+}
+
+std::string NetworkTable::GetString(StringRef key,
+                                    StringRef defaultValue) const {
+  return GetEntry(key).GetString(defaultValue);
+}
+
+bool NetworkTable::PutBoolean(StringRef key, bool value) {
+  return GetEntry(key).SetBoolean(value);
+}
+
+bool NetworkTable::SetDefaultBoolean(StringRef key, bool defaultValue) {
+  return GetEntry(key).SetDefaultBoolean(defaultValue);
+}
+
+bool NetworkTable::GetBoolean(StringRef key, bool defaultValue) const {
+  return GetEntry(key).GetBoolean(defaultValue);
+}
+
+bool NetworkTable::PutBooleanArray(StringRef key, ArrayRef<int> value) {
+  return GetEntry(key).SetBooleanArray(value);
+}
+
+bool NetworkTable::SetDefaultBooleanArray(StringRef key,
+                                          ArrayRef<int> defaultValue) {
+  return GetEntry(key).SetDefaultBooleanArray(defaultValue);
+}
+
+std::vector<int> NetworkTable::GetBooleanArray(
+    StringRef key, ArrayRef<int> defaultValue) const {
+  return GetEntry(key).GetBooleanArray(defaultValue);
+}
+
+bool NetworkTable::PutNumberArray(StringRef key, ArrayRef<double> value) {
+  return GetEntry(key).SetDoubleArray(value);
+}
+
+bool NetworkTable::SetDefaultNumberArray(StringRef key,
+                                         ArrayRef<double> defaultValue) {
+  return GetEntry(key).SetDefaultDoubleArray(defaultValue);
+}
+
+std::vector<double> NetworkTable::GetNumberArray(
+    StringRef key, ArrayRef<double> defaultValue) const {
+  return GetEntry(key).GetDoubleArray(defaultValue);
+}
+
+bool NetworkTable::PutStringArray(StringRef key, ArrayRef<std::string> value) {
+  return GetEntry(key).SetStringArray(value);
+}
+
+bool NetworkTable::SetDefaultStringArray(StringRef key,
+                                         ArrayRef<std::string> defaultValue) {
+  return GetEntry(key).SetDefaultStringArray(defaultValue);
+}
+
+std::vector<std::string> NetworkTable::GetStringArray(
+    StringRef key, ArrayRef<std::string> defaultValue) const {
+  return GetEntry(key).GetStringArray(defaultValue);
+}
+
+bool NetworkTable::PutRaw(StringRef key, StringRef value) {
+  return GetEntry(key).SetRaw(value);
+}
+
+bool NetworkTable::SetDefaultRaw(StringRef key, StringRef defaultValue) {
+  return GetEntry(key).SetDefaultRaw(defaultValue);
+}
+
+std::string NetworkTable::GetRaw(StringRef key, StringRef defaultValue) const {
+  return GetEntry(key).GetRaw(defaultValue);
+}
+
+bool NetworkTable::PutValue(const Twine& key, std::shared_ptr<Value> value) {
+  return GetEntry(key).SetValue(value);
+}
+
+bool NetworkTable::SetDefaultValue(const Twine& key,
+                                   std::shared_ptr<Value> defaultValue) {
+  return GetEntry(key).SetDefaultValue(defaultValue);
+}
+
+std::shared_ptr<Value> NetworkTable::GetValue(const Twine& key) const {
+  return GetEntry(key).GetValue();
+}
+
+StringRef NetworkTable::GetPath() const { return m_path; }
+
+const char* NetworkTable::SaveEntries(const Twine& filename) const {
+  return nt::SaveEntries(m_inst, filename, m_path + Twine(PATH_SEPARATOR_CHAR));
+}
+
+const char* NetworkTable::LoadEntries(
+    const Twine& filename,
+    std::function<void(size_t line, const char* msg)> warn) {
+  return nt::LoadEntries(m_inst, filename, m_path + Twine(PATH_SEPARATOR_CHAR),
+                         warn);
+}
diff --git a/ntcore/src/main/native/cpp/networktables/NetworkTableEntry.cpp b/ntcore/src/main/native/cpp/networktables/NetworkTableEntry.cpp
new file mode 100644
index 0000000..5507ac0
--- /dev/null
+++ b/ntcore/src/main/native/cpp/networktables/NetworkTableEntry.cpp
@@ -0,0 +1,16 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "networktables/NetworkTableEntry.h"
+
+#include "networktables/NetworkTableInstance.h"
+
+using namespace nt;
+
+NetworkTableInstance NetworkTableEntry::GetInstance() const {
+  return NetworkTableInstance{GetInstanceFromHandle(m_handle)};
+}
diff --git a/ntcore/src/main/native/cpp/networktables/NetworkTableInstance.cpp b/ntcore/src/main/native/cpp/networktables/NetworkTableInstance.cpp
new file mode 100644
index 0000000..018572e
--- /dev/null
+++ b/ntcore/src/main/native/cpp/networktables/NetworkTableInstance.cpp
@@ -0,0 +1,59 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "networktables/NetworkTableInstance.h"
+
+#include <wpi/SmallString.h>
+
+using namespace nt;
+
+std::shared_ptr<NetworkTable> NetworkTableInstance::GetTable(
+    const Twine& key) const {
+  StringRef simple;
+  bool isSimple = key.isSingleStringRef();
+  if (isSimple) simple = key.getSingleStringRef();
+  if (isSimple && (simple.empty() || simple == "/")) {
+    return std::make_shared<NetworkTable>(m_handle, "",
+                                          NetworkTable::private_init{});
+  } else if (isSimple && simple[0] == NetworkTable::PATH_SEPARATOR_CHAR) {
+    return std::make_shared<NetworkTable>(m_handle, key,
+                                          NetworkTable::private_init{});
+  } else {
+    return std::make_shared<NetworkTable>(
+        m_handle, Twine(NetworkTable::PATH_SEPARATOR_CHAR) + key,
+        NetworkTable::private_init{});
+  }
+}
+
+void NetworkTableInstance::StartClient(ArrayRef<StringRef> servers,
+                                       unsigned int port) {
+  wpi::SmallVector<std::pair<StringRef, unsigned int>, 8> server_ports;
+  for (const auto& server : servers)
+    server_ports.emplace_back(std::make_pair(server, port));
+  StartClient(server_ports);
+}
+
+void NetworkTableInstance::SetServer(ArrayRef<StringRef> servers,
+                                     unsigned int port) {
+  wpi::SmallVector<std::pair<StringRef, unsigned int>, 8> server_ports;
+  for (const auto& server : servers)
+    server_ports.emplace_back(std::make_pair(server, port));
+  SetServer(server_ports);
+}
+
+NT_EntryListener NetworkTableInstance::AddEntryListener(
+    const Twine& prefix,
+    std::function<void(const EntryNotification& event)> callback,
+    unsigned int flags) const {
+  return ::nt::AddEntryListener(m_handle, prefix, callback, flags);
+}
+
+NT_ConnectionListener NetworkTableInstance::AddConnectionListener(
+    std::function<void(const ConnectionNotification& event)> callback,
+    bool immediate_notify) const {
+  return ::nt::AddConnectionListener(m_handle, callback, immediate_notify);
+}
diff --git a/ntcore/src/main/native/cpp/networktables/RpcCall.cpp b/ntcore/src/main/native/cpp/networktables/RpcCall.cpp
new file mode 100644
index 0000000..1149681
--- /dev/null
+++ b/ntcore/src/main/native/cpp/networktables/RpcCall.cpp
@@ -0,0 +1,16 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "networktables/RpcCall.h"
+
+#include "networktables/NetworkTableEntry.h"
+
+using namespace nt;
+
+NetworkTableEntry RpcCall::GetEntry() const {
+  return NetworkTableEntry{m_entry};
+}
diff --git a/ntcore/src/main/native/cpp/ntcore_c.cpp b/ntcore/src/main/native/cpp/ntcore_c.cpp
new file mode 100644
index 0000000..6c71e55
--- /dev/null
+++ b/ntcore/src/main/native/cpp/ntcore_c.cpp
@@ -0,0 +1,1136 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <stdint.h>
+
+#include <cassert>
+#include <cstdlib>
+
+#include <wpi/memory.h>
+#include <wpi/timestamp.h>
+
+#include "Value_internal.h"
+#include "ntcore.h"
+
+using namespace nt;
+
+// Conversion helpers
+
+static void ConvertToC(wpi::StringRef in, char** out) {
+  *out = static_cast<char*>(wpi::CheckedMalloc(in.size() + 1));
+  std::memmove(*out, in.data(), in.size());
+  (*out)[in.size()] = '\0';
+}
+
+static void ConvertToC(const EntryInfo& in, NT_EntryInfo* out) {
+  out->entry = in.entry;
+  ConvertToC(in.name, &out->name);
+  out->type = in.type;
+  out->flags = in.flags;
+  out->last_change = in.last_change;
+}
+
+static void ConvertToC(const ConnectionInfo& in, NT_ConnectionInfo* out) {
+  ConvertToC(in.remote_id, &out->remote_id);
+  ConvertToC(in.remote_ip, &out->remote_ip);
+  out->remote_port = in.remote_port;
+  out->last_update = in.last_update;
+  out->protocol_version = in.protocol_version;
+}
+
+static void ConvertToC(const RpcParamDef& in, NT_RpcParamDef* out) {
+  ConvertToC(in.name, &out->name);
+  ConvertToC(*in.def_value, &out->def_value);
+}
+
+static void ConvertToC(const RpcResultDef& in, NT_RpcResultDef* out) {
+  ConvertToC(in.name, &out->name);
+  out->type = in.type;
+}
+
+static void ConvertToC(const RpcDefinition& in, NT_RpcDefinition* out) {
+  out->version = in.version;
+  ConvertToC(in.name, &out->name);
+
+  out->num_params = in.params.size();
+  out->params = static_cast<NT_RpcParamDef*>(
+      wpi::CheckedMalloc(in.params.size() * sizeof(NT_RpcParamDef)));
+  for (size_t i = 0; i < in.params.size(); ++i)
+    ConvertToC(in.params[i], &out->params[i]);
+
+  out->num_results = in.results.size();
+  out->results = static_cast<NT_RpcResultDef*>(
+      wpi::CheckedMalloc(in.results.size() * sizeof(NT_RpcResultDef)));
+  for (size_t i = 0; i < in.results.size(); ++i)
+    ConvertToC(in.results[i], &out->results[i]);
+}
+
+static void ConvertToC(const RpcAnswer& in, NT_RpcAnswer* out) {
+  out->entry = in.entry;
+  out->call = in.call;
+  ConvertToC(in.name, &out->name);
+  ConvertToC(in.params, &out->params);
+  ConvertToC(in.conn, &out->conn);
+}
+
+static void ConvertToC(const EntryNotification& in, NT_EntryNotification* out) {
+  out->listener = in.listener;
+  out->entry = in.entry;
+  ConvertToC(in.name, &out->name);
+  ConvertToC(*in.value, &out->value);
+  out->flags = in.flags;
+}
+
+static void ConvertToC(const ConnectionNotification& in,
+                       NT_ConnectionNotification* out) {
+  out->listener = in.listener;
+  out->connected = in.connected;
+  ConvertToC(in.conn, &out->conn);
+}
+
+static void ConvertToC(const LogMessage& in, NT_LogMessage* out) {
+  out->logger = in.logger;
+  out->level = in.level;
+  out->filename = in.filename;
+  out->line = in.line;
+  ConvertToC(in.message, &out->message);
+}
+
+template <typename O, typename I>
+static O* ConvertToC(const std::vector<I>& in, size_t* out_len) {
+  if (!out_len) return nullptr;
+  *out_len = in.size();
+  if (in.empty()) return nullptr;
+  O* out = static_cast<O*>(wpi::CheckedMalloc(sizeof(O) * in.size()));
+  for (size_t i = 0; i < in.size(); ++i) ConvertToC(in[i], &out[i]);
+  return out;
+}
+
+static void DisposeConnectionInfo(NT_ConnectionInfo* info) {
+  std::free(info->remote_id.str);
+  std::free(info->remote_ip.str);
+}
+
+static void DisposeEntryInfo(NT_EntryInfo* info) { std::free(info->name.str); }
+
+static void DisposeEntryNotification(NT_EntryNotification* info) {
+  std::free(info->name.str);
+  NT_DisposeValue(&info->value);
+}
+
+static void DisposeConnectionNotification(NT_ConnectionNotification* info) {
+  DisposeConnectionInfo(&info->conn);
+}
+
+static RpcParamDef ConvertFromC(const NT_RpcParamDef& in) {
+  RpcParamDef out;
+  out.name = ConvertFromC(in.name);
+  out.def_value = ConvertFromC(in.def_value);
+  return out;
+}
+
+static RpcResultDef ConvertFromC(const NT_RpcResultDef& in) {
+  RpcResultDef out;
+  out.name = ConvertFromC(in.name);
+  out.type = in.type;
+  return out;
+}
+
+static RpcDefinition ConvertFromC(const NT_RpcDefinition& in) {
+  RpcDefinition out;
+  out.version = in.version;
+  out.name = ConvertFromC(in.name);
+
+  out.params.reserve(in.num_params);
+  for (size_t i = 0; i < in.num_params; ++i)
+    out.params.push_back(ConvertFromC(in.params[i]));
+
+  out.results.reserve(in.num_results);
+  for (size_t i = 0; i < in.num_results; ++i)
+    out.results.push_back(ConvertFromC(in.results[i]));
+
+  return out;
+}
+
+extern "C" {
+
+/*
+ * Instance Functions
+ */
+
+NT_Inst NT_GetDefaultInstance(void) { return nt::GetDefaultInstance(); }
+
+NT_Inst NT_CreateInstance(void) { return nt::CreateInstance(); }
+
+void NT_DestroyInstance(NT_Inst inst) { return nt::DestroyInstance(inst); }
+
+NT_Inst NT_GetInstanceFromHandle(NT_Handle handle) {
+  return nt::GetInstanceFromHandle(handle);
+}
+
+/*
+ * Table Functions
+ */
+
+NT_Entry NT_GetEntry(NT_Inst inst, const char* name, size_t name_len) {
+  return nt::GetEntry(inst, StringRef(name, name_len));
+}
+
+NT_Entry* NT_GetEntries(NT_Inst inst, const char* prefix, size_t prefix_len,
+                        unsigned int types, size_t* count) {
+  auto info_v = nt::GetEntries(inst, StringRef(prefix, prefix_len), types);
+  *count = info_v.size();
+  if (info_v.size() == 0) return nullptr;
+
+  // create array and copy into it
+  NT_Entry* info = static_cast<NT_Entry*>(
+      wpi::CheckedMalloc(info_v.size() * sizeof(NT_Entry)));
+  std::memcpy(info, info_v.data(), info_v.size() * sizeof(NT_Entry));
+  return info;
+}
+
+char* NT_GetEntryName(NT_Entry entry, size_t* name_len) {
+  struct NT_String v_name;
+  nt::ConvertToC(nt::GetEntryName(entry), &v_name);
+  *name_len = v_name.len;
+  return v_name.str;
+}
+
+enum NT_Type NT_GetEntryType(NT_Entry entry) { return nt::GetEntryType(entry); }
+
+uint64_t NT_GetEntryLastChange(NT_Entry entry) {
+  return nt::GetEntryLastChange(entry);
+}
+
+void NT_GetEntryValue(NT_Entry entry, struct NT_Value* value) {
+  NT_InitValue(value);
+  auto v = nt::GetEntryValue(entry);
+  if (!v) return;
+  ConvertToC(*v, value);
+}
+
+int NT_SetDefaultEntryValue(NT_Entry entry,
+                            const struct NT_Value* default_value) {
+  return nt::SetDefaultEntryValue(entry, ConvertFromC(*default_value));
+}
+
+int NT_SetEntryValue(NT_Entry entry, const struct NT_Value* value) {
+  return nt::SetEntryValue(entry, ConvertFromC(*value));
+}
+
+void NT_SetEntryTypeValue(NT_Entry entry, const struct NT_Value* value) {
+  nt::SetEntryTypeValue(entry, ConvertFromC(*value));
+}
+
+void NT_SetEntryFlags(NT_Entry entry, unsigned int flags) {
+  nt::SetEntryFlags(entry, flags);
+}
+
+unsigned int NT_GetEntryFlags(NT_Entry entry) {
+  return nt::GetEntryFlags(entry);
+}
+
+void NT_DeleteEntry(NT_Entry entry) { nt::DeleteEntry(entry); }
+
+void NT_DeleteAllEntries(NT_Inst inst) { nt::DeleteAllEntries(inst); }
+
+struct NT_EntryInfo* NT_GetEntryInfo(NT_Inst inst, const char* prefix,
+                                     size_t prefix_len, unsigned int types,
+                                     size_t* count) {
+  auto info_v = nt::GetEntryInfo(inst, StringRef(prefix, prefix_len), types);
+  return ConvertToC<NT_EntryInfo>(info_v, count);
+}
+
+NT_Bool NT_GetEntryInfoHandle(NT_Entry entry, struct NT_EntryInfo* info) {
+  auto info_v = nt::GetEntryInfo(entry);
+  if (info_v.name.empty()) return false;
+  ConvertToC(info_v, info);
+  return true;
+}
+
+/*
+ * Callback Creation Functions
+ */
+
+NT_EntryListener NT_AddEntryListener(NT_Inst inst, const char* prefix,
+                                     size_t prefix_len, void* data,
+                                     NT_EntryListenerCallback callback,
+                                     unsigned int flags) {
+  return nt::AddEntryListener(inst, StringRef(prefix, prefix_len),
+                              [=](const EntryNotification& event) {
+                                NT_EntryNotification c_event;
+                                ConvertToC(event, &c_event);
+                                callback(data, &c_event);
+                                DisposeEntryNotification(&c_event);
+                              },
+                              flags);
+}
+
+NT_EntryListener NT_AddEntryListenerSingle(NT_Entry entry, void* data,
+                                           NT_EntryListenerCallback callback,
+                                           unsigned int flags) {
+  return nt::AddEntryListener(entry,
+                              [=](const EntryNotification& event) {
+                                NT_EntryNotification c_event;
+                                ConvertToC(event, &c_event);
+                                callback(data, &c_event);
+                                DisposeEntryNotification(&c_event);
+                              },
+                              flags);
+}
+
+NT_EntryListenerPoller NT_CreateEntryListenerPoller(NT_Inst inst) {
+  return nt::CreateEntryListenerPoller(inst);
+}
+
+void NT_DestroyEntryListenerPoller(NT_EntryListenerPoller poller) {
+  nt::DestroyEntryListenerPoller(poller);
+}
+
+NT_EntryListener NT_AddPolledEntryListener(NT_EntryListenerPoller poller,
+                                           const char* prefix,
+                                           size_t prefix_len,
+                                           unsigned int flags) {
+  return nt::AddPolledEntryListener(poller, StringRef(prefix, prefix_len),
+                                    flags);
+}
+
+NT_EntryListener NT_AddPolledEntryListenerSingle(NT_EntryListenerPoller poller,
+                                                 NT_Entry entry,
+                                                 unsigned int flags) {
+  return nt::AddPolledEntryListener(poller, entry, flags);
+}
+
+struct NT_EntryNotification* NT_PollEntryListener(NT_EntryListenerPoller poller,
+                                                  size_t* len) {
+  auto arr_cpp = nt::PollEntryListener(poller);
+  return ConvertToC<NT_EntryNotification>(arr_cpp, len);
+}
+
+struct NT_EntryNotification* NT_PollEntryListenerTimeout(
+    NT_EntryListenerPoller poller, size_t* len, double timeout,
+    NT_Bool* timed_out) {
+  bool cpp_timed_out = false;
+  auto arr_cpp = nt::PollEntryListener(poller, timeout, &cpp_timed_out);
+  *timed_out = cpp_timed_out;
+  return ConvertToC<NT_EntryNotification>(arr_cpp, len);
+}
+
+void NT_CancelPollEntryListener(NT_EntryListenerPoller poller) {
+  nt::CancelPollEntryListener(poller);
+}
+
+void NT_RemoveEntryListener(NT_EntryListener entry_listener) {
+  nt::RemoveEntryListener(entry_listener);
+}
+
+NT_Bool NT_WaitForEntryListenerQueue(NT_Inst inst, double timeout) {
+  return nt::WaitForEntryListenerQueue(inst, timeout);
+}
+
+NT_ConnectionListener NT_AddConnectionListener(
+    NT_Inst inst, void* data, NT_ConnectionListenerCallback callback,
+    NT_Bool immediate_notify) {
+  return nt::AddConnectionListener(inst,
+                                   [=](const ConnectionNotification& event) {
+                                     NT_ConnectionNotification event_c;
+                                     ConvertToC(event, &event_c);
+                                     callback(data, &event_c);
+                                     DisposeConnectionNotification(&event_c);
+                                   },
+                                   immediate_notify != 0);
+}
+
+NT_ConnectionListenerPoller NT_CreateConnectionListenerPoller(NT_Inst inst) {
+  return nt::CreateConnectionListenerPoller(inst);
+}
+
+void NT_DestroyConnectionListenerPoller(NT_ConnectionListenerPoller poller) {
+  nt::DestroyConnectionListenerPoller(poller);
+}
+
+NT_ConnectionListener NT_AddPolledConnectionListener(
+    NT_ConnectionListenerPoller poller, NT_Bool immediate_notify) {
+  return nt::AddPolledConnectionListener(poller, immediate_notify);
+}
+
+struct NT_ConnectionNotification* NT_PollConnectionListener(
+    NT_ConnectionListenerPoller poller, size_t* len) {
+  auto arr_cpp = nt::PollConnectionListener(poller);
+  return ConvertToC<NT_ConnectionNotification>(arr_cpp, len);
+}
+
+struct NT_ConnectionNotification* NT_PollConnectionListenerTimeout(
+    NT_ConnectionListenerPoller poller, size_t* len, double timeout,
+    NT_Bool* timed_out) {
+  bool cpp_timed_out = false;
+  auto arr_cpp = nt::PollConnectionListener(poller, timeout, &cpp_timed_out);
+  *timed_out = cpp_timed_out;
+  return ConvertToC<NT_ConnectionNotification>(arr_cpp, len);
+}
+
+void NT_CancelPollConnectionListener(NT_ConnectionListenerPoller poller) {
+  nt::CancelPollConnectionListener(poller);
+}
+
+void NT_RemoveConnectionListener(NT_ConnectionListener conn_listener) {
+  nt::RemoveConnectionListener(conn_listener);
+}
+
+NT_Bool NT_WaitForConnectionListenerQueue(NT_Inst inst, double timeout) {
+  return nt::WaitForConnectionListenerQueue(inst, timeout);
+}
+
+/*
+ * Remote Procedure Call Functions
+ */
+
+void NT_CreateRpc(NT_Entry entry, const char* def, size_t def_len, void* data,
+                  NT_RpcCallback callback) {
+  nt::CreateRpc(entry, StringRef(def, def_len), [=](const RpcAnswer& answer) {
+    NT_RpcAnswer answer_c;
+    ConvertToC(answer, &answer_c);
+    callback(data, &answer_c);
+    NT_DisposeRpcAnswer(&answer_c);
+  });
+}
+
+NT_RpcCallPoller NT_CreateRpcCallPoller(NT_Inst inst) {
+  return nt::CreateRpcCallPoller(inst);
+}
+
+void NT_DestroyRpcCallPoller(NT_RpcCallPoller poller) {
+  nt::DestroyRpcCallPoller(poller);
+}
+
+void NT_CreatePolledRpc(NT_Entry entry, const char* def, size_t def_len,
+                        NT_RpcCallPoller poller) {
+  nt::CreatePolledRpc(entry, StringRef(def, def_len), poller);
+}
+
+NT_RpcAnswer* NT_PollRpc(NT_RpcCallPoller poller, size_t* len) {
+  auto arr_cpp = nt::PollRpc(poller);
+  return ConvertToC<NT_RpcAnswer>(arr_cpp, len);
+}
+
+NT_RpcAnswer* NT_PollRpcTimeout(NT_RpcCallPoller poller, size_t* len,
+                                double timeout, NT_Bool* timed_out) {
+  bool cpp_timed_out = false;
+  auto arr_cpp = nt::PollRpc(poller, timeout, &cpp_timed_out);
+  *timed_out = cpp_timed_out;
+  return ConvertToC<NT_RpcAnswer>(arr_cpp, len);
+}
+
+void NT_CancelPollRpc(NT_RpcCallPoller poller) { nt::CancelPollRpc(poller); }
+
+NT_Bool NT_WaitForRpcCallQueue(NT_Inst inst, double timeout) {
+  return nt::WaitForRpcCallQueue(inst, timeout);
+}
+
+NT_Bool NT_PostRpcResponse(NT_Entry entry, NT_RpcCall call, const char* result,
+                           size_t result_len) {
+  return nt::PostRpcResponse(entry, call, StringRef(result, result_len));
+}
+
+NT_RpcCall NT_CallRpc(NT_Entry entry, const char* params, size_t params_len) {
+  return nt::CallRpc(entry, StringRef(params, params_len));
+}
+
+char* NT_GetRpcResult(NT_Entry entry, NT_RpcCall call, size_t* result_len) {
+  std::string result;
+  if (!nt::GetRpcResult(entry, call, &result)) return nullptr;
+
+  // convert result
+  *result_len = result.size();
+  char* result_cstr;
+  ConvertToC(result, &result_cstr);
+  return result_cstr;
+}
+
+char* NT_GetRpcResultTimeout(NT_Entry entry, NT_RpcCall call,
+                             size_t* result_len, double timeout,
+                             NT_Bool* timed_out) {
+  std::string result;
+  bool cpp_timed_out = false;
+  if (!nt::GetRpcResult(entry, call, &result, timeout, &cpp_timed_out)) {
+    *timed_out = cpp_timed_out;
+    return nullptr;
+  }
+
+  *timed_out = cpp_timed_out;
+  // convert result
+  *result_len = result.size();
+  char* result_cstr;
+  ConvertToC(result, &result_cstr);
+  return result_cstr;
+}
+
+void NT_CancelRpcResult(NT_Entry entry, NT_RpcCall call) {
+  nt::CancelRpcResult(entry, call);
+}
+
+char* NT_PackRpcDefinition(const NT_RpcDefinition* def, size_t* packed_len) {
+  auto packed = nt::PackRpcDefinition(ConvertFromC(*def));
+
+  // convert result
+  *packed_len = packed.size();
+  char* packed_cstr;
+  ConvertToC(packed, &packed_cstr);
+  return packed_cstr;
+}
+
+NT_Bool NT_UnpackRpcDefinition(const char* packed, size_t packed_len,
+                               NT_RpcDefinition* def) {
+  nt::RpcDefinition def_v;
+  if (!nt::UnpackRpcDefinition(StringRef(packed, packed_len), &def_v)) return 0;
+
+  // convert result
+  ConvertToC(def_v, def);
+  return 1;
+}
+
+char* NT_PackRpcValues(const NT_Value** values, size_t values_len,
+                       size_t* packed_len) {
+  // create input vector
+  std::vector<std::shared_ptr<Value>> values_v;
+  values_v.reserve(values_len);
+  for (size_t i = 0; i < values_len; ++i)
+    values_v.push_back(ConvertFromC(*values[i]));
+
+  // make the call
+  auto packed = nt::PackRpcValues(values_v);
+
+  // convert result
+  *packed_len = packed.size();
+  char* packed_cstr;
+  ConvertToC(packed, &packed_cstr);
+  return packed_cstr;
+}
+
+NT_Value** NT_UnpackRpcValues(const char* packed, size_t packed_len,
+                              const NT_Type* types, size_t types_len) {
+  auto values_v = nt::UnpackRpcValues(StringRef(packed, packed_len),
+                                      ArrayRef<NT_Type>(types, types_len));
+  if (values_v.size() == 0) return nullptr;
+
+  // create array and copy into it
+  NT_Value** values = static_cast<NT_Value**>(
+      wpi::CheckedMalloc(values_v.size() * sizeof(NT_Value*)));
+  for (size_t i = 0; i < values_v.size(); ++i) {
+    values[i] = static_cast<NT_Value*>(wpi::CheckedMalloc(sizeof(NT_Value)));
+    ConvertToC(*values_v[i], values[i]);
+  }
+  return values;
+}
+
+/*
+ * Client/Server Functions
+ */
+
+void NT_SetNetworkIdentity(NT_Inst inst, const char* name, size_t name_len) {
+  nt::SetNetworkIdentity(inst, StringRef(name, name_len));
+}
+
+unsigned int NT_GetNetworkMode(NT_Inst inst) {
+  return nt::GetNetworkMode(inst);
+}
+
+void NT_StartServer(NT_Inst inst, const char* persist_filename,
+                    const char* listen_address, unsigned int port) {
+  nt::StartServer(inst, persist_filename, listen_address, port);
+}
+
+void NT_StopServer(NT_Inst inst) { nt::StopServer(inst); }
+
+void NT_StartClientNone(NT_Inst inst) { nt::StartClient(inst); }
+
+void NT_StartClient(NT_Inst inst, const char* server_name, unsigned int port) {
+  nt::StartClient(inst, server_name, port);
+}
+
+void NT_StartClientMulti(NT_Inst inst, size_t count, const char** server_names,
+                         const unsigned int* ports) {
+  std::vector<std::pair<StringRef, unsigned int>> servers;
+  servers.reserve(count);
+  for (size_t i = 0; i < count; ++i)
+    servers.emplace_back(std::make_pair(server_names[i], ports[i]));
+  nt::StartClient(inst, servers);
+}
+
+void NT_StartClientTeam(NT_Inst inst, unsigned int team, unsigned int port) {
+  nt::StartClientTeam(inst, team, port);
+}
+
+void NT_StopClient(NT_Inst inst) { nt::StopClient(inst); }
+
+void NT_SetServer(NT_Inst inst, const char* server_name, unsigned int port) {
+  nt::SetServer(inst, server_name, port);
+}
+
+void NT_SetServerMulti(NT_Inst inst, size_t count, const char** server_names,
+                       const unsigned int* ports) {
+  std::vector<std::pair<StringRef, unsigned int>> servers;
+  servers.reserve(count);
+  for (size_t i = 0; i < count; ++i)
+    servers.emplace_back(std::make_pair(server_names[i], ports[i]));
+  nt::SetServer(inst, servers);
+}
+
+void NT_SetServerTeam(NT_Inst inst, unsigned int team, unsigned int port) {
+  nt::SetServerTeam(inst, team, port);
+}
+
+void NT_StartDSClient(NT_Inst inst, unsigned int port) {
+  nt::StartDSClient(inst, port);
+}
+
+void NT_StopDSClient(NT_Inst inst) { nt::StopDSClient(inst); }
+
+void NT_SetUpdateRate(NT_Inst inst, double interval) {
+  nt::SetUpdateRate(inst, interval);
+}
+
+void NT_Flush(NT_Inst inst) { nt::Flush(inst); }
+
+NT_Bool NT_IsConnected(NT_Inst inst) { return nt::IsConnected(inst); }
+
+struct NT_ConnectionInfo* NT_GetConnections(NT_Inst inst, size_t* count) {
+  auto conn_v = nt::GetConnections(inst);
+  return ConvertToC<NT_ConnectionInfo>(conn_v, count);
+}
+
+/*
+ * File Save/Load Functions
+ */
+
+const char* NT_SavePersistent(NT_Inst inst, const char* filename) {
+  return nt::SavePersistent(inst, filename);
+}
+
+const char* NT_LoadPersistent(NT_Inst inst, const char* filename,
+                              void (*warn)(size_t line, const char* msg)) {
+  return nt::LoadPersistent(inst, filename, warn);
+}
+
+const char* NT_SaveEntries(NT_Inst inst, const char* filename,
+                           const char* prefix, size_t prefix_len) {
+  return nt::SaveEntries(inst, filename, StringRef(prefix, prefix_len));
+}
+
+const char* NT_LoadEntries(NT_Inst inst, const char* filename,
+                           const char* prefix, size_t prefix_len,
+                           void (*warn)(size_t line, const char* msg)) {
+  return nt::LoadEntries(inst, filename, StringRef(prefix, prefix_len), warn);
+}
+
+/*
+ * Utility Functions
+ */
+
+uint64_t NT_Now(void) { return wpi::Now(); }
+
+NT_Logger NT_AddLogger(NT_Inst inst, void* data, NT_LogFunc func,
+                       unsigned int min_level, unsigned int max_level) {
+  return nt::AddLogger(inst,
+                       [=](const LogMessage& msg) {
+                         NT_LogMessage msg_c;
+                         ConvertToC(msg, &msg_c);
+                         func(data, &msg_c);
+                         NT_DisposeLogMessage(&msg_c);
+                       },
+                       min_level, max_level);
+}
+
+NT_LoggerPoller NT_CreateLoggerPoller(NT_Inst inst) {
+  return nt::CreateLoggerPoller(inst);
+}
+
+void NT_DestroyLoggerPoller(NT_LoggerPoller poller) {
+  nt::DestroyLoggerPoller(poller);
+}
+
+NT_Logger NT_AddPolledLogger(NT_LoggerPoller poller, unsigned int min_level,
+                             unsigned int max_level) {
+  return nt::AddPolledLogger(poller, min_level, max_level);
+}
+
+struct NT_LogMessage* NT_PollLogger(NT_LoggerPoller poller, size_t* len) {
+  auto arr_cpp = nt::PollLogger(poller);
+  return ConvertToC<NT_LogMessage>(arr_cpp, len);
+}
+
+struct NT_LogMessage* NT_PollLoggerTimeout(NT_LoggerPoller poller, size_t* len,
+                                           double timeout, NT_Bool* timed_out) {
+  bool cpp_timed_out = false;
+  auto arr_cpp = nt::PollLogger(poller, timeout, &cpp_timed_out);
+  *timed_out = cpp_timed_out;
+  return ConvertToC<NT_LogMessage>(arr_cpp, len);
+}
+
+void NT_CancelPollLogger(NT_LoggerPoller poller) {
+  nt::CancelPollLogger(poller);
+}
+
+void NT_RemoveLogger(NT_Logger logger) { nt::RemoveLogger(logger); }
+
+NT_Bool NT_WaitForLoggerQueue(NT_Inst inst, double timeout) {
+  return nt::WaitForLoggerQueue(inst, timeout);
+}
+
+void NT_DisposeValue(NT_Value* value) {
+  switch (value->type) {
+    case NT_UNASSIGNED:
+    case NT_BOOLEAN:
+    case NT_DOUBLE:
+      break;
+    case NT_STRING:
+    case NT_RAW:
+    case NT_RPC:
+      std::free(value->data.v_string.str);
+      break;
+    case NT_BOOLEAN_ARRAY:
+      std::free(value->data.arr_boolean.arr);
+      break;
+    case NT_DOUBLE_ARRAY:
+      std::free(value->data.arr_double.arr);
+      break;
+    case NT_STRING_ARRAY: {
+      for (size_t i = 0; i < value->data.arr_string.size; i++)
+        std::free(value->data.arr_string.arr[i].str);
+      std::free(value->data.arr_string.arr);
+      break;
+    }
+    default:
+      assert(false && "unknown value type");
+  }
+  value->type = NT_UNASSIGNED;
+  value->last_change = 0;
+}
+
+void NT_InitValue(NT_Value* value) {
+  value->type = NT_UNASSIGNED;
+  value->last_change = 0;
+}
+
+void NT_DisposeString(NT_String* str) {
+  std::free(str->str);
+  str->str = nullptr;
+  str->len = 0;
+}
+
+void NT_InitString(NT_String* str) {
+  str->str = nullptr;
+  str->len = 0;
+}
+
+void NT_DisposeEntryArray(NT_Entry* arr, size_t /*count*/) { std::free(arr); }
+
+void NT_DisposeConnectionInfoArray(NT_ConnectionInfo* arr, size_t count) {
+  for (size_t i = 0; i < count; i++) DisposeConnectionInfo(&arr[i]);
+  std::free(arr);
+}
+
+void NT_DisposeEntryInfoArray(NT_EntryInfo* arr, size_t count) {
+  for (size_t i = 0; i < count; i++) DisposeEntryInfo(&arr[i]);
+  std::free(arr);
+}
+
+void NT_DisposeEntryInfo(NT_EntryInfo* info) { DisposeEntryInfo(info); }
+
+void NT_DisposeEntryNotificationArray(NT_EntryNotification* arr, size_t count) {
+  for (size_t i = 0; i < count; i++) DisposeEntryNotification(&arr[i]);
+  std::free(arr);
+}
+
+void NT_DisposeEntryNotification(NT_EntryNotification* info) {
+  DisposeEntryNotification(info);
+}
+
+void NT_DisposeConnectionNotificationArray(NT_ConnectionNotification* arr,
+                                           size_t count) {
+  for (size_t i = 0; i < count; i++) DisposeConnectionNotification(&arr[i]);
+  std::free(arr);
+}
+
+void NT_DisposeConnectionNotification(NT_ConnectionNotification* info) {
+  DisposeConnectionNotification(info);
+}
+
+void NT_DisposeLogMessageArray(NT_LogMessage* arr, size_t count) {
+  for (size_t i = 0; i < count; i++) NT_DisposeLogMessage(&arr[i]);
+  std::free(arr);
+}
+
+void NT_DisposeLogMessage(NT_LogMessage* info) { std::free(info->message); }
+
+void NT_DisposeRpcDefinition(NT_RpcDefinition* def) {
+  NT_DisposeString(&def->name);
+
+  for (size_t i = 0; i < def->num_params; ++i) {
+    NT_DisposeString(&def->params[i].name);
+    NT_DisposeValue(&def->params[i].def_value);
+  }
+  std::free(def->params);
+  def->params = nullptr;
+  def->num_params = 0;
+
+  for (size_t i = 0; i < def->num_results; ++i)
+    NT_DisposeString(&def->results[i].name);
+  std::free(def->results);
+  def->results = nullptr;
+  def->num_results = 0;
+}
+
+void NT_DisposeRpcAnswerArray(NT_RpcAnswer* arr, size_t count) {
+  for (size_t i = 0; i < count; i++) NT_DisposeRpcAnswer(&arr[i]);
+  std::free(arr);
+}
+
+void NT_DisposeRpcAnswer(NT_RpcAnswer* call_info) {
+  NT_DisposeString(&call_info->name);
+  NT_DisposeString(&call_info->params);
+  DisposeConnectionInfo(&call_info->conn);
+}
+
+/* Interop Utility Functions */
+
+/* Array and Struct Allocations */
+
+/* Allocates a char array of the specified size.*/
+char* NT_AllocateCharArray(size_t size) {
+  char* retVal = static_cast<char*>(wpi::CheckedMalloc(size * sizeof(char)));
+  return retVal;
+}
+
+/* Allocates an integer or boolean array of the specified size. */
+int* NT_AllocateBooleanArray(size_t size) {
+  int* retVal = static_cast<int*>(wpi::CheckedMalloc(size * sizeof(int)));
+  return retVal;
+}
+
+/* Allocates a double array of the specified size. */
+double* NT_AllocateDoubleArray(size_t size) {
+  double* retVal =
+      static_cast<double*>(wpi::CheckedMalloc(size * sizeof(double)));
+  return retVal;
+}
+
+/* Allocates an NT_String array of the specified size. */
+struct NT_String* NT_AllocateStringArray(size_t size) {
+  NT_String* retVal =
+      static_cast<NT_String*>(wpi::CheckedMalloc(size * sizeof(NT_String)));
+  return retVal;
+}
+
+void NT_FreeCharArray(char* v_char) { std::free(v_char); }
+void NT_FreeDoubleArray(double* v_double) { std::free(v_double); }
+void NT_FreeBooleanArray(int* v_boolean) { std::free(v_boolean); }
+void NT_FreeStringArray(struct NT_String* v_string, size_t arr_size) {
+  for (size_t i = 0; i < arr_size; i++) std::free(v_string[i].str);
+  std::free(v_string);
+}
+
+NT_Bool NT_SetEntryDouble(NT_Entry entry, uint64_t time, double v_double,
+                          NT_Bool force) {
+  if (force != 0) {
+    nt::SetEntryTypeValue(entry, Value::MakeDouble(v_double, time));
+    return 1;
+  } else {
+    return nt::SetEntryValue(entry, Value::MakeDouble(v_double, time));
+  }
+}
+
+NT_Bool NT_SetEntryBoolean(NT_Entry entry, uint64_t time, NT_Bool v_boolean,
+                           NT_Bool force) {
+  if (force != 0) {
+    nt::SetEntryTypeValue(entry, Value::MakeBoolean(v_boolean != 0, time));
+    return 1;
+  } else {
+    return nt::SetEntryValue(entry, Value::MakeBoolean(v_boolean != 0, time));
+  }
+}
+
+NT_Bool NT_SetEntryString(NT_Entry entry, uint64_t time, const char* str,
+                          size_t str_len, NT_Bool force) {
+  if (force != 0) {
+    nt::SetEntryTypeValue(entry,
+                          Value::MakeString(StringRef(str, str_len), time));
+    return 1;
+  } else {
+    return nt::SetEntryValue(entry,
+                             Value::MakeString(StringRef(str, str_len), time));
+  }
+}
+
+NT_Bool NT_SetEntryRaw(NT_Entry entry, uint64_t time, const char* raw,
+                       size_t raw_len, NT_Bool force) {
+  if (force != 0) {
+    nt::SetEntryTypeValue(entry, Value::MakeRaw(StringRef(raw, raw_len), time));
+    return 1;
+  } else {
+    return nt::SetEntryValue(entry,
+                             Value::MakeRaw(StringRef(raw, raw_len), time));
+  }
+}
+
+NT_Bool NT_SetEntryBooleanArray(NT_Entry entry, uint64_t time,
+                                const NT_Bool* arr, size_t size,
+                                NT_Bool force) {
+  if (force != 0) {
+    nt::SetEntryTypeValue(
+        entry, Value::MakeBooleanArray(wpi::makeArrayRef(arr, size), time));
+    return 1;
+  } else {
+    return nt::SetEntryValue(
+        entry, Value::MakeBooleanArray(wpi::makeArrayRef(arr, size), time));
+  }
+}
+
+NT_Bool NT_SetEntryDoubleArray(NT_Entry entry, uint64_t time, const double* arr,
+                               size_t size, NT_Bool force) {
+  if (force != 0) {
+    nt::SetEntryTypeValue(
+        entry, Value::MakeDoubleArray(wpi::makeArrayRef(arr, size), time));
+    return 1;
+  } else {
+    return nt::SetEntryValue(
+        entry, Value::MakeDoubleArray(wpi::makeArrayRef(arr, size), time));
+  }
+}
+
+NT_Bool NT_SetEntryStringArray(NT_Entry entry, uint64_t time,
+                               const struct NT_String* arr, size_t size,
+                               NT_Bool force) {
+  std::vector<std::string> v;
+  v.reserve(size);
+  for (size_t i = 0; i < size; ++i) v.push_back(ConvertFromC(arr[i]));
+
+  if (force != 0) {
+    nt::SetEntryTypeValue(entry, Value::MakeStringArray(std::move(v), time));
+    return 1;
+  } else {
+    return nt::SetEntryValue(entry, Value::MakeStringArray(std::move(v), time));
+  }
+}
+
+enum NT_Type NT_GetValueType(const struct NT_Value* value) {
+  if (!value) return NT_Type::NT_UNASSIGNED;
+  return value->type;
+}
+
+NT_Bool NT_GetValueBoolean(const struct NT_Value* value, uint64_t* last_change,
+                           NT_Bool* v_boolean) {
+  if (!value || value->type != NT_Type::NT_BOOLEAN) return 0;
+  *v_boolean = value->data.v_boolean;
+  *last_change = value->last_change;
+  return 1;
+}
+
+NT_Bool NT_GetValueDouble(const struct NT_Value* value, uint64_t* last_change,
+                          double* v_double) {
+  if (!value || value->type != NT_Type::NT_DOUBLE) return 0;
+  *last_change = value->last_change;
+  *v_double = value->data.v_double;
+  return 1;
+}
+
+char* NT_GetValueString(const struct NT_Value* value, uint64_t* last_change,
+                        size_t* str_len) {
+  if (!value || value->type != NT_Type::NT_STRING) return nullptr;
+  *last_change = value->last_change;
+  *str_len = value->data.v_string.len;
+  char* str =
+      static_cast<char*>(wpi::CheckedMalloc(value->data.v_string.len + 1));
+  std::memcpy(str, value->data.v_string.str, value->data.v_string.len + 1);
+  return str;
+}
+
+char* NT_GetValueRaw(const struct NT_Value* value, uint64_t* last_change,
+                     size_t* raw_len) {
+  if (!value || value->type != NT_Type::NT_RAW) return nullptr;
+  *last_change = value->last_change;
+  *raw_len = value->data.v_string.len;
+  char* raw =
+      static_cast<char*>(wpi::CheckedMalloc(value->data.v_string.len + 1));
+  std::memcpy(raw, value->data.v_string.str, value->data.v_string.len + 1);
+  return raw;
+}
+
+NT_Bool* NT_GetValueBooleanArray(const struct NT_Value* value,
+                                 uint64_t* last_change, size_t* arr_size) {
+  if (!value || value->type != NT_Type::NT_BOOLEAN_ARRAY) return nullptr;
+  *last_change = value->last_change;
+  *arr_size = value->data.arr_boolean.size;
+  NT_Bool* arr = static_cast<int*>(
+      wpi::CheckedMalloc(value->data.arr_boolean.size * sizeof(NT_Bool)));
+  std::memcpy(arr, value->data.arr_boolean.arr,
+              value->data.arr_boolean.size * sizeof(NT_Bool));
+  return arr;
+}
+
+double* NT_GetValueDoubleArray(const struct NT_Value* value,
+                               uint64_t* last_change, size_t* arr_size) {
+  if (!value || value->type != NT_Type::NT_DOUBLE_ARRAY) return nullptr;
+  *last_change = value->last_change;
+  *arr_size = value->data.arr_double.size;
+  double* arr = static_cast<double*>(
+      wpi::CheckedMalloc(value->data.arr_double.size * sizeof(double)));
+  std::memcpy(arr, value->data.arr_double.arr,
+              value->data.arr_double.size * sizeof(double));
+  return arr;
+}
+
+NT_String* NT_GetValueStringArray(const struct NT_Value* value,
+                                  uint64_t* last_change, size_t* arr_size) {
+  if (!value || value->type != NT_Type::NT_STRING_ARRAY) return nullptr;
+  *last_change = value->last_change;
+  *arr_size = value->data.arr_string.size;
+  NT_String* arr = static_cast<NT_String*>(
+      wpi::CheckedMalloc(value->data.arr_string.size * sizeof(NT_String)));
+  for (size_t i = 0; i < value->data.arr_string.size; ++i) {
+    size_t len = value->data.arr_string.arr[i].len;
+    arr[i].len = len;
+    arr[i].str = static_cast<char*>(wpi::CheckedMalloc(len + 1));
+    std::memcpy(arr[i].str, value->data.arr_string.arr[i].str, len + 1);
+  }
+  return arr;
+}
+
+NT_Bool NT_SetDefaultEntryBoolean(NT_Entry entry, uint64_t time,
+                                  NT_Bool default_boolean) {
+  return nt::SetDefaultEntryValue(
+      entry, Value::MakeBoolean(default_boolean != 0, time));
+}
+
+NT_Bool NT_SetDefaultEntryDouble(NT_Entry entry, uint64_t time,
+                                 double default_double) {
+  return nt::SetDefaultEntryValue(entry,
+                                  Value::MakeDouble(default_double, time));
+}
+
+NT_Bool NT_SetDefaultEntryString(NT_Entry entry, uint64_t time,
+                                 const char* default_value,
+                                 size_t default_len) {
+  return nt::SetDefaultEntryValue(
+      entry, Value::MakeString(StringRef(default_value, default_len), time));
+}
+
+NT_Bool NT_SetDefaultEntryRaw(NT_Entry entry, uint64_t time,
+                              const char* default_value, size_t default_len) {
+  return nt::SetDefaultEntryValue(
+      entry, Value::MakeRaw(StringRef(default_value, default_len), time));
+}
+
+NT_Bool NT_SetDefaultEntryBooleanArray(NT_Entry entry, uint64_t time,
+                                       const NT_Bool* default_value,
+                                       size_t default_size) {
+  return nt::SetDefaultEntryValue(
+      entry, Value::MakeBooleanArray(
+                 wpi::makeArrayRef(default_value, default_size), time));
+}
+
+NT_Bool NT_SetDefaultEntryDoubleArray(NT_Entry entry, uint64_t time,
+                                      const double* default_value,
+                                      size_t default_size) {
+  return nt::SetDefaultEntryValue(
+      entry, Value::MakeDoubleArray(
+                 wpi::makeArrayRef(default_value, default_size), time));
+}
+
+NT_Bool NT_SetDefaultEntryStringArray(NT_Entry entry, uint64_t time,
+                                      const struct NT_String* default_value,
+                                      size_t default_size) {
+  std::vector<std::string> vec;
+  vec.reserve(default_size);
+  for (size_t i = 0; i < default_size; ++i)
+    vec.push_back(ConvertFromC(default_value[i]));
+
+  return nt::SetDefaultEntryValue(entry,
+                                  Value::MakeStringArray(std::move(vec), time));
+}
+
+NT_Bool NT_GetEntryBoolean(NT_Entry entry, uint64_t* last_change,
+                           NT_Bool* v_boolean) {
+  auto v = nt::GetEntryValue(entry);
+  if (!v || !v->IsBoolean()) return 0;
+  *v_boolean = v->GetBoolean();
+  *last_change = v->last_change();
+  return 1;
+}
+
+NT_Bool NT_GetEntryDouble(NT_Entry entry, uint64_t* last_change,
+                          double* v_double) {
+  auto v = nt::GetEntryValue(entry);
+  if (!v || !v->IsDouble()) return 0;
+  *last_change = v->last_change();
+  *v_double = v->GetDouble();
+  return 1;
+}
+
+char* NT_GetEntryString(NT_Entry entry, uint64_t* last_change,
+                        size_t* str_len) {
+  auto v = nt::GetEntryValue(entry);
+  if (!v || !v->IsString()) return nullptr;
+  *last_change = v->last_change();
+  struct NT_String v_string;
+  nt::ConvertToC(v->GetString(), &v_string);
+  *str_len = v_string.len;
+  return v_string.str;
+}
+
+char* NT_GetEntryRaw(NT_Entry entry, uint64_t* last_change, size_t* raw_len) {
+  auto v = nt::GetEntryValue(entry);
+  if (!v || !v->IsRaw()) return nullptr;
+  *last_change = v->last_change();
+  struct NT_String v_raw;
+  nt::ConvertToC(v->GetRaw(), &v_raw);
+  *raw_len = v_raw.len;
+  return v_raw.str;
+}
+
+NT_Bool* NT_GetEntryBooleanArray(NT_Entry entry, uint64_t* last_change,
+                                 size_t* arr_size) {
+  auto v = nt::GetEntryValue(entry);
+  if (!v || !v->IsBooleanArray()) return nullptr;
+  *last_change = v->last_change();
+  auto vArr = v->GetBooleanArray();
+  NT_Bool* arr =
+      static_cast<int*>(wpi::CheckedMalloc(vArr.size() * sizeof(NT_Bool)));
+  *arr_size = vArr.size();
+  std::copy(vArr.begin(), vArr.end(), arr);
+  return arr;
+}
+
+double* NT_GetEntryDoubleArray(NT_Entry entry, uint64_t* last_change,
+                               size_t* arr_size) {
+  auto v = nt::GetEntryValue(entry);
+  if (!v || !v->IsDoubleArray()) return nullptr;
+  *last_change = v->last_change();
+  auto vArr = v->GetDoubleArray();
+  double* arr =
+      static_cast<double*>(wpi::CheckedMalloc(vArr.size() * sizeof(double)));
+  *arr_size = vArr.size();
+  std::copy(vArr.begin(), vArr.end(), arr);
+  return arr;
+}
+
+NT_String* NT_GetEntryStringArray(NT_Entry entry, uint64_t* last_change,
+                                  size_t* arr_size) {
+  auto v = nt::GetEntryValue(entry);
+  if (!v || !v->IsStringArray()) return nullptr;
+  *last_change = v->last_change();
+  auto vArr = v->GetStringArray();
+  NT_String* arr = static_cast<NT_String*>(
+      wpi::CheckedMalloc(vArr.size() * sizeof(NT_String)));
+  for (size_t i = 0; i < vArr.size(); ++i) {
+    ConvertToC(vArr[i], &arr[i]);
+  }
+  *arr_size = vArr.size();
+  return arr;
+}
+
+}  // extern "C"
diff --git a/ntcore/src/main/native/cpp/ntcore_cpp.cpp b/ntcore/src/main/native/cpp/ntcore_cpp.cpp
new file mode 100644
index 0000000..18256f5
--- /dev/null
+++ b/ntcore/src/main/native/cpp/ntcore_cpp.cpp
@@ -0,0 +1,1062 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <stdint.h>
+
+#include <cassert>
+#include <cstdio>
+#include <cstdlib>
+
+#include <wpi/timestamp.h>
+
+#include "Handle.h"
+#include "InstanceImpl.h"
+#include "Log.h"
+#include "WireDecoder.h"
+#include "WireEncoder.h"
+#include "ntcore.h"
+
+namespace nt {
+
+/*
+ * Instance Functions
+ */
+
+NT_Inst GetDefaultInstance() {
+  return Handle{InstanceImpl::GetDefaultIndex(), 0, Handle::kInstance};
+}
+
+NT_Inst CreateInstance() {
+  return Handle{InstanceImpl::Alloc(), 0, Handle::kInstance};
+}
+
+void DestroyInstance(NT_Inst inst) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  if (i < 0) return;
+  InstanceImpl::Destroy(i);
+}
+
+NT_Inst GetInstanceFromHandle(NT_Handle handle) {
+  Handle h{handle};
+  auto type = h.GetType();
+  if (type >= Handle::kConnectionListener && type <= Handle::kRpcCallPoller)
+    return Handle(h.GetInst(), 0, Handle::kInstance);
+
+  return 0;
+}
+
+/*
+ * Table Functions
+ */
+
+NT_Entry GetEntry(NT_Inst inst, const Twine& name) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (!ii) return 0;
+
+  unsigned int id = ii->storage.GetEntry(name);
+  if (id == UINT_MAX) return 0;
+  return Handle(i, id, Handle::kEntry);
+}
+
+std::vector<NT_Entry> GetEntries(NT_Inst inst, const Twine& prefix,
+                                 unsigned int types) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (!ii) return std::vector<NT_Entry>{};
+
+  auto arr = ii->storage.GetEntries(prefix, types);
+  // convert indices to handles
+  for (auto& val : arr) val = Handle(i, val, Handle::kEntry);
+  return arr;
+}
+
+std::string GetEntryName(NT_Entry entry) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return std::string{};
+
+  return ii->storage.GetEntryName(id);
+}
+
+NT_Type GetEntryType(NT_Entry entry) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return NT_UNASSIGNED;
+
+  return ii->storage.GetEntryType(id);
+}
+
+uint64_t GetEntryLastChange(NT_Entry entry) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return 0;
+
+  return ii->storage.GetEntryLastChange(id);
+}
+
+std::shared_ptr<Value> GetEntryValue(StringRef name) {
+  return InstanceImpl::GetDefault()->storage.GetEntryValue(name);
+}
+
+std::shared_ptr<Value> GetEntryValue(NT_Entry entry) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return nullptr;
+
+  return ii->storage.GetEntryValue(id);
+}
+
+bool SetDefaultEntryValue(StringRef name, std::shared_ptr<Value> value) {
+  return InstanceImpl::GetDefault()->storage.SetDefaultEntryValue(name, value);
+}
+
+bool SetDefaultEntryValue(NT_Entry entry, std::shared_ptr<Value> value) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return false;
+
+  return ii->storage.SetDefaultEntryValue(id, value);
+}
+
+bool SetEntryValue(StringRef name, std::shared_ptr<Value> value) {
+  return InstanceImpl::GetDefault()->storage.SetEntryValue(name, value);
+}
+
+bool SetEntryValue(NT_Entry entry, std::shared_ptr<Value> value) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return false;
+
+  return ii->storage.SetEntryValue(id, value);
+}
+
+void SetEntryTypeValue(StringRef name, std::shared_ptr<Value> value) {
+  InstanceImpl::GetDefault()->storage.SetEntryTypeValue(name, value);
+}
+
+void SetEntryTypeValue(NT_Entry entry, std::shared_ptr<Value> value) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  ii->storage.SetEntryTypeValue(id, value);
+}
+
+void SetEntryFlags(StringRef name, unsigned int flags) {
+  InstanceImpl::GetDefault()->storage.SetEntryFlags(name, flags);
+}
+
+void SetEntryFlags(NT_Entry entry, unsigned int flags) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  ii->storage.SetEntryFlags(id, flags);
+}
+
+unsigned int GetEntryFlags(StringRef name) {
+  return InstanceImpl::GetDefault()->storage.GetEntryFlags(name);
+}
+
+unsigned int GetEntryFlags(NT_Entry entry) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return 0;
+
+  return ii->storage.GetEntryFlags(id);
+}
+
+void DeleteEntry(StringRef name) {
+  InstanceImpl::GetDefault()->storage.DeleteEntry(name);
+}
+
+void DeleteEntry(NT_Entry entry) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  ii->storage.DeleteEntry(id);
+}
+
+void DeleteAllEntries() {
+  InstanceImpl::GetDefault()->storage.DeleteAllEntries();
+}
+
+void DeleteAllEntries(NT_Inst inst) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (i < 0 || !ii) return;
+
+  ii->storage.DeleteAllEntries();
+}
+
+std::vector<EntryInfo> GetEntryInfo(StringRef prefix, unsigned int types) {
+  return InstanceImpl::GetDefault()->storage.GetEntryInfo(0, prefix, types);
+}
+
+std::vector<EntryInfo> GetEntryInfo(NT_Inst inst, const Twine& prefix,
+                                    unsigned int types) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (!ii) return std::vector<EntryInfo>{};
+
+  return ii->storage.GetEntryInfo(i, prefix, types);
+}
+
+EntryInfo GetEntryInfo(NT_Entry entry) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  int i = handle.GetInst();
+  auto ii = InstanceImpl::Get(i);
+  if (id < 0 || !ii) {
+    EntryInfo info;
+    info.entry = 0;
+    info.type = NT_UNASSIGNED;
+    info.flags = 0;
+    info.last_change = 0;
+    return info;
+  }
+
+  return ii->storage.GetEntryInfo(i, id);
+}
+
+/*
+ * Callback Creation Functions
+ */
+
+NT_EntryListener AddEntryListener(StringRef prefix,
+                                  EntryListenerCallback callback,
+                                  unsigned int flags) {
+  return AddEntryListener(
+      Handle(InstanceImpl::GetDefaultIndex(), 0, Handle::kInstance), prefix,
+      [=](const EntryNotification& event) {
+        callback(event.listener, event.name, event.value, event.flags);
+      },
+      flags);
+}
+
+NT_EntryListener AddEntryListener(
+    NT_Inst inst, const Twine& prefix,
+    std::function<void(const EntryNotification& event)> callback,
+    unsigned int flags) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (i < 0 || !ii) return 0;
+
+  unsigned int uid = ii->storage.AddListener(prefix, callback, flags);
+  return Handle(i, uid, Handle::kEntryListener);
+}
+
+NT_EntryListener AddEntryListener(
+    NT_Entry entry,
+    std::function<void(const EntryNotification& event)> callback,
+    unsigned int flags) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  int i = handle.GetInst();
+  auto ii = InstanceImpl::Get(i);
+  if (id < 0 || !ii) return 0;
+
+  unsigned int uid = ii->storage.AddListener(id, callback, flags);
+  return Handle(i, uid, Handle::kEntryListener);
+}
+
+NT_EntryListenerPoller CreateEntryListenerPoller(NT_Inst inst) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (!ii) return 0;
+
+  return Handle(i, ii->entry_notifier.CreatePoller(),
+                Handle::kEntryListenerPoller);
+}
+
+void DestroyEntryListenerPoller(NT_EntryListenerPoller poller) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kEntryListenerPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  ii->entry_notifier.RemovePoller(id);
+}
+
+NT_EntryListener AddPolledEntryListener(NT_EntryListenerPoller poller,
+                                        const Twine& prefix,
+                                        unsigned int flags) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kEntryListenerPoller);
+  int i = handle.GetInst();
+  auto ii = InstanceImpl::Get(i);
+  if (id < 0 || !ii) return 0;
+
+  unsigned int uid = ii->storage.AddPolledListener(id, prefix, flags);
+  return Handle(i, uid, Handle::kEntryListener);
+}
+
+NT_EntryListener AddPolledEntryListener(NT_EntryListenerPoller poller,
+                                        NT_Entry entry, unsigned int flags) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  int i = handle.GetInst();
+  auto ii = InstanceImpl::Get(i);
+  if (id < 0 || !ii) return 0;
+
+  Handle phandle{poller};
+  int p_id = phandle.GetTypedIndex(Handle::kEntryListenerPoller);
+  if (p_id < 0) return 0;
+  if (handle.GetInst() != phandle.GetInst()) return 0;
+
+  unsigned int uid = ii->storage.AddPolledListener(p_id, id, flags);
+  return Handle(i, uid, Handle::kEntryListener);
+}
+
+std::vector<EntryNotification> PollEntryListener(
+    NT_EntryListenerPoller poller) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kEntryListenerPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return std::vector<EntryNotification>{};
+
+  return ii->entry_notifier.Poll(static_cast<unsigned int>(id));
+}
+
+std::vector<EntryNotification> PollEntryListener(NT_EntryListenerPoller poller,
+                                                 double timeout,
+                                                 bool* timed_out) {
+  *timed_out = false;
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kEntryListenerPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return std::vector<EntryNotification>{};
+
+  return ii->entry_notifier.Poll(static_cast<unsigned int>(id), timeout,
+                                 timed_out);
+}
+
+void CancelPollEntryListener(NT_EntryListenerPoller poller) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kEntryListenerPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  ii->entry_notifier.CancelPoll(id);
+}
+
+void RemoveEntryListener(NT_EntryListener entry_listener) {
+  Handle handle{entry_listener};
+  int uid = handle.GetTypedIndex(Handle::kEntryListener);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (uid < 0 || !ii) return;
+
+  ii->entry_notifier.Remove(uid);
+}
+
+bool WaitForEntryListenerQueue(NT_Inst inst, double timeout) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (!ii) return true;
+  return ii->entry_notifier.WaitForQueue(timeout);
+}
+
+NT_ConnectionListener AddConnectionListener(ConnectionListenerCallback callback,
+                                            bool immediate_notify) {
+  return AddConnectionListener(
+      Handle(InstanceImpl::GetDefaultIndex(), 0, Handle::kInstance),
+      [=](const ConnectionNotification& event) {
+        callback(event.listener, event.connected, event.conn);
+      },
+      immediate_notify);
+}
+
+NT_ConnectionListener AddConnectionListener(
+    NT_Inst inst,
+    std::function<void(const ConnectionNotification& event)> callback,
+    bool immediate_notify) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (!ii) return 0;
+
+  unsigned int uid = ii->dispatcher.AddListener(callback, immediate_notify);
+  return Handle(i, uid, Handle::kConnectionListener);
+}
+
+NT_ConnectionListenerPoller CreateConnectionListenerPoller(NT_Inst inst) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (!ii) return 0;
+
+  return Handle(i, ii->connection_notifier.CreatePoller(),
+                Handle::kConnectionListenerPoller);
+}
+
+void DestroyConnectionListenerPoller(NT_ConnectionListenerPoller poller) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kConnectionListenerPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  ii->connection_notifier.RemovePoller(id);
+}
+
+NT_ConnectionListener AddPolledConnectionListener(
+    NT_ConnectionListenerPoller poller, bool immediate_notify) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kConnectionListenerPoller);
+  int i = handle.GetInst();
+  auto ii = InstanceImpl::Get(i);
+  if (id < 0 || !ii) return 0;
+
+  unsigned int uid = ii->dispatcher.AddPolledListener(id, immediate_notify);
+  return Handle(i, uid, Handle::kConnectionListener);
+}
+
+std::vector<ConnectionNotification> PollConnectionListener(
+    NT_ConnectionListenerPoller poller) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kConnectionListenerPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return std::vector<ConnectionNotification>{};
+
+  return ii->connection_notifier.Poll(static_cast<unsigned int>(id));
+}
+
+std::vector<ConnectionNotification> PollConnectionListener(
+    NT_ConnectionListenerPoller poller, double timeout, bool* timed_out) {
+  *timed_out = false;
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kConnectionListenerPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return std::vector<ConnectionNotification>{};
+
+  return ii->connection_notifier.Poll(static_cast<unsigned int>(id), timeout,
+                                      timed_out);
+}
+
+void CancelPollConnectionListener(NT_ConnectionListenerPoller poller) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kConnectionListenerPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  ii->connection_notifier.CancelPoll(id);
+}
+
+void RemoveConnectionListener(NT_ConnectionListener conn_listener) {
+  Handle handle{conn_listener};
+  int uid = handle.GetTypedIndex(Handle::kConnectionListener);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (uid < 0 || !ii) return;
+
+  ii->connection_notifier.Remove(uid);
+}
+
+bool WaitForConnectionListenerQueue(NT_Inst inst, double timeout) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (!ii) return true;
+  return ii->connection_notifier.WaitForQueue(timeout);
+}
+
+/*
+ * Remote Procedure Call Functions
+ */
+
+void CreateRpc(NT_Entry entry, StringRef def,
+               std::function<void(const RpcAnswer& answer)> callback) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  // only server can create RPCs
+  if ((ii->dispatcher.GetNetworkMode() & NT_NET_MODE_SERVER) == 0) return;
+  if (def.empty() || !callback) return;
+
+  ii->storage.CreateRpc(id, def, ii->rpc_server.Add(callback));
+}
+
+NT_RpcCallPoller CreateRpcCallPoller(NT_Inst inst) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (!ii) return 0;
+
+  return Handle(i, ii->rpc_server.CreatePoller(), Handle::kRpcCallPoller);
+}
+
+void DestroyRpcCallPoller(NT_RpcCallPoller poller) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kRpcCallPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  ii->rpc_server.RemovePoller(id);
+}
+
+void CreatePolledRpc(NT_Entry entry, StringRef def, NT_RpcCallPoller poller) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  Handle phandle{poller};
+  int p_id = phandle.GetTypedIndex(Handle::kRpcCallPoller);
+  if (p_id < 0) return;
+  if (handle.GetInst() != phandle.GetInst()) return;
+
+  // only server can create RPCs
+  if ((ii->dispatcher.GetNetworkMode() & NT_NET_MODE_SERVER) == 0) return;
+  if (def.empty()) return;
+
+  ii->storage.CreateRpc(id, def, ii->rpc_server.AddPolled(p_id));
+}
+
+std::vector<RpcAnswer> PollRpc(NT_RpcCallPoller poller) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kRpcCallPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return std::vector<RpcAnswer>{};
+
+  return ii->rpc_server.Poll(static_cast<unsigned int>(id));
+}
+
+std::vector<RpcAnswer> PollRpc(NT_RpcCallPoller poller, double timeout,
+                               bool* timed_out) {
+  *timed_out = false;
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kRpcCallPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return std::vector<RpcAnswer>{};
+
+  return ii->rpc_server.Poll(static_cast<unsigned int>(id), timeout, timed_out);
+}
+
+void CancelPollRpc(NT_RpcCallPoller poller) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kRpcCallPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  ii->rpc_server.CancelPoll(id);
+}
+
+bool WaitForRpcCallQueue(NT_Inst inst, double timeout) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (!ii) return true;
+  return ii->rpc_server.WaitForQueue(timeout);
+}
+
+bool PostRpcResponse(NT_Entry entry, NT_RpcCall call, StringRef result) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return false;
+
+  Handle chandle{call};
+  int call_uid = chandle.GetTypedIndex(Handle::kRpcCall);
+  if (call_uid < 0) return false;
+  if (handle.GetInst() != chandle.GetInst()) return false;
+
+  return ii->rpc_server.PostRpcResponse(id, call_uid, result);
+}
+
+NT_RpcCall CallRpc(NT_Entry entry, StringRef params) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  int i = handle.GetInst();
+  auto ii = InstanceImpl::Get(i);
+  if (id < 0 || !ii) return 0;
+
+  unsigned int call_uid = ii->storage.CallRpc(id, params);
+  if (call_uid == 0) return 0;
+  return Handle(i, call_uid, Handle::kRpcCall);
+}
+
+bool GetRpcResult(NT_Entry entry, NT_RpcCall call, std::string* result) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return false;
+
+  Handle chandle{call};
+  int call_uid = chandle.GetTypedIndex(Handle::kRpcCall);
+  if (call_uid < 0) return false;
+  if (handle.GetInst() != chandle.GetInst()) return false;
+
+  return ii->storage.GetRpcResult(id, call_uid, result);
+}
+
+bool GetRpcResult(NT_Entry entry, NT_RpcCall call, std::string* result,
+                  double timeout, bool* timed_out) {
+  *timed_out = false;
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return false;
+
+  Handle chandle{call};
+  int call_uid = chandle.GetTypedIndex(Handle::kRpcCall);
+  if (call_uid < 0) return false;
+  if (handle.GetInst() != chandle.GetInst()) return false;
+
+  return ii->storage.GetRpcResult(id, call_uid, result, timeout, timed_out);
+}
+
+void CancelRpcResult(NT_Entry entry, NT_RpcCall call) {
+  Handle handle{entry};
+  int id = handle.GetTypedIndex(Handle::kEntry);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  Handle chandle{call};
+  int call_uid = chandle.GetTypedIndex(Handle::kRpcCall);
+  if (call_uid < 0) return;
+  if (handle.GetInst() != chandle.GetInst()) return;
+
+  ii->storage.CancelRpcResult(id, call_uid);
+}
+
+std::string PackRpcDefinition(const RpcDefinition& def) {
+  WireEncoder enc(0x0300);
+  enc.Write8(def.version);
+  enc.WriteString(def.name);
+
+  // parameters
+  unsigned int params_size = def.params.size();
+  if (params_size > 0xff) params_size = 0xff;
+  enc.Write8(params_size);
+  for (size_t i = 0; i < params_size; ++i) {
+    enc.WriteType(def.params[i].def_value->type());
+    enc.WriteString(def.params[i].name);
+    enc.WriteValue(*def.params[i].def_value);
+  }
+
+  // results
+  unsigned int results_size = def.results.size();
+  if (results_size > 0xff) results_size = 0xff;
+  enc.Write8(results_size);
+  for (size_t i = 0; i < results_size; ++i) {
+    enc.WriteType(def.results[i].type);
+    enc.WriteString(def.results[i].name);
+  }
+
+  return enc.ToStringRef();
+}
+
+bool UnpackRpcDefinition(StringRef packed, RpcDefinition* def) {
+  wpi::raw_mem_istream is(packed.data(), packed.size());
+  wpi::Logger logger;
+  WireDecoder dec(is, 0x0300, logger);
+  if (!dec.Read8(&def->version)) return false;
+  if (!dec.ReadString(&def->name)) return false;
+
+  // parameters
+  unsigned int params_size;
+  if (!dec.Read8(&params_size)) return false;
+  def->params.resize(0);
+  def->params.reserve(params_size);
+  for (size_t i = 0; i < params_size; ++i) {
+    RpcParamDef pdef;
+    NT_Type type;
+    if (!dec.ReadType(&type)) return false;
+    if (!dec.ReadString(&pdef.name)) return false;
+    pdef.def_value = dec.ReadValue(type);
+    if (!pdef.def_value) return false;
+    def->params.emplace_back(std::move(pdef));
+  }
+
+  // results
+  unsigned int results_size;
+  if (!dec.Read8(&results_size)) return false;
+  def->results.resize(0);
+  def->results.reserve(results_size);
+  for (size_t i = 0; i < results_size; ++i) {
+    RpcResultDef rdef;
+    if (!dec.ReadType(&rdef.type)) return false;
+    if (!dec.ReadString(&rdef.name)) return false;
+    def->results.emplace_back(std::move(rdef));
+  }
+
+  return true;
+}
+
+std::string PackRpcValues(ArrayRef<std::shared_ptr<Value>> values) {
+  WireEncoder enc(0x0300);
+  for (auto& value : values) enc.WriteValue(*value);
+  return enc.ToStringRef();
+}
+
+std::vector<std::shared_ptr<Value>> UnpackRpcValues(StringRef packed,
+                                                    ArrayRef<NT_Type> types) {
+  wpi::raw_mem_istream is(packed.data(), packed.size());
+  wpi::Logger logger;
+  WireDecoder dec(is, 0x0300, logger);
+  std::vector<std::shared_ptr<Value>> vec;
+  for (auto type : types) {
+    auto item = dec.ReadValue(type);
+    if (!item) return std::vector<std::shared_ptr<Value>>();
+    vec.emplace_back(std::move(item));
+  }
+  return vec;
+}
+
+uint64_t Now() { return wpi::Now(); }
+
+/*
+ * Client/Server Functions
+ */
+
+void SetNetworkIdentity(StringRef name) {
+  InstanceImpl::GetDefault()->dispatcher.SetIdentity(name);
+}
+
+void SetNetworkIdentity(NT_Inst inst, const Twine& name) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->dispatcher.SetIdentity(name);
+}
+
+unsigned int GetNetworkMode() {
+  return InstanceImpl::GetDefault()->dispatcher.GetNetworkMode();
+}
+
+unsigned int GetNetworkMode(NT_Inst inst) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return 0;
+
+  return ii->dispatcher.GetNetworkMode();
+}
+
+void StartServer(StringRef persist_filename, const char* listen_address,
+                 unsigned int port) {
+  auto ii = InstanceImpl::GetDefault();
+  ii->dispatcher.StartServer(persist_filename, listen_address, port);
+}
+
+void StartServer(NT_Inst inst, const Twine& persist_filename,
+                 const char* listen_address, unsigned int port) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->dispatcher.StartServer(persist_filename, listen_address, port);
+}
+
+void StopServer() { InstanceImpl::GetDefault()->dispatcher.Stop(); }
+
+void StopServer(NT_Inst inst) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->dispatcher.Stop();
+}
+
+void StartClient() { InstanceImpl::GetDefault()->dispatcher.StartClient(); }
+
+void StartClient(NT_Inst inst) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->dispatcher.StartClient();
+}
+
+void StartClient(const char* server_name, unsigned int port) {
+  auto ii = InstanceImpl::GetDefault();
+  ii->dispatcher.SetServer(server_name, port);
+  ii->dispatcher.StartClient();
+}
+
+void StartClient(NT_Inst inst, const char* server_name, unsigned int port) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->dispatcher.SetServer(server_name, port);
+  ii->dispatcher.StartClient();
+}
+
+void StartClient(ArrayRef<std::pair<StringRef, unsigned int>> servers) {
+  auto ii = InstanceImpl::GetDefault();
+  ii->dispatcher.SetServer(servers);
+  ii->dispatcher.StartClient();
+}
+
+void StartClient(NT_Inst inst,
+                 ArrayRef<std::pair<StringRef, unsigned int>> servers) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->dispatcher.SetServer(servers);
+  ii->dispatcher.StartClient();
+}
+
+void StartClientTeam(NT_Inst inst, unsigned int team, unsigned int port) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->dispatcher.SetServerTeam(team, port);
+  ii->dispatcher.StartClient();
+}
+
+void StopClient() { InstanceImpl::GetDefault()->dispatcher.Stop(); }
+
+void StopClient(NT_Inst inst) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->dispatcher.Stop();
+}
+
+void SetServer(const char* server_name, unsigned int port) {
+  InstanceImpl::GetDefault()->dispatcher.SetServer(server_name, port);
+}
+
+void SetServer(NT_Inst inst, const char* server_name, unsigned int port) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->dispatcher.SetServer(server_name, port);
+}
+
+void SetServer(ArrayRef<std::pair<StringRef, unsigned int>> servers) {
+  InstanceImpl::GetDefault()->dispatcher.SetServer(servers);
+}
+
+void SetServer(NT_Inst inst,
+               ArrayRef<std::pair<StringRef, unsigned int>> servers) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->dispatcher.SetServer(servers);
+}
+
+void SetServerTeam(NT_Inst inst, unsigned int team, unsigned int port) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->dispatcher.SetServerTeam(team, port);
+}
+
+void StartDSClient(unsigned int port) {
+  InstanceImpl::GetDefault()->ds_client.Start(port);
+}
+
+void StartDSClient(NT_Inst inst, unsigned int port) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->ds_client.Start(port);
+}
+
+void StopDSClient() { InstanceImpl::GetDefault()->ds_client.Stop(); }
+
+void StopDSClient(NT_Inst inst) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->ds_client.Stop();
+}
+
+void SetUpdateRate(double interval) {
+  InstanceImpl::GetDefault()->dispatcher.SetUpdateRate(interval);
+}
+
+void SetUpdateRate(NT_Inst inst, double interval) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->dispatcher.SetUpdateRate(interval);
+}
+
+void Flush() { InstanceImpl::GetDefault()->dispatcher.Flush(); }
+
+void Flush(NT_Inst inst) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return;
+
+  ii->dispatcher.Flush();
+}
+
+std::vector<ConnectionInfo> GetConnections() {
+  return InstanceImpl::GetDefault()->dispatcher.GetConnections();
+}
+
+std::vector<ConnectionInfo> GetConnections(NT_Inst inst) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return std::vector<ConnectionInfo>{};
+
+  return ii->dispatcher.GetConnections();
+}
+
+bool IsConnected(NT_Inst inst) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return false;
+
+  return ii->dispatcher.IsConnected();
+}
+
+/*
+ * Persistent Functions
+ */
+
+const char* SavePersistent(StringRef filename) {
+  return InstanceImpl::GetDefault()->storage.SavePersistent(filename, false);
+}
+
+const char* SavePersistent(NT_Inst inst, const Twine& filename) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return "invalid instance handle";
+
+  return ii->storage.SavePersistent(filename, false);
+}
+
+const char* LoadPersistent(
+    StringRef filename,
+    std::function<void(size_t line, const char* msg)> warn) {
+  return InstanceImpl::GetDefault()->storage.LoadPersistent(filename, warn);
+}
+
+const char* LoadPersistent(
+    NT_Inst inst, const Twine& filename,
+    std::function<void(size_t line, const char* msg)> warn) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return "invalid instance handle";
+
+  return ii->storage.LoadPersistent(filename, warn);
+}
+
+const char* SaveEntries(NT_Inst inst, const Twine& filename,
+                        const Twine& prefix) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return "invalid instance handle";
+
+  return ii->storage.SaveEntries(filename, prefix);
+}
+
+const char* LoadEntries(
+    NT_Inst inst, const Twine& filename, const Twine& prefix,
+    std::function<void(size_t line, const char* msg)> warn) {
+  auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
+  if (!ii) return "invalid instance handle";
+
+  return ii->storage.LoadEntries(filename, prefix, warn);
+}
+
+void SetLogger(LogFunc func, unsigned int min_level) {
+  auto ii = InstanceImpl::GetDefault();
+  static wpi::mutex mutex;
+  static unsigned int logger = 0;
+  std::lock_guard<wpi::mutex> lock(mutex);
+  if (logger != 0) ii->logger_impl.Remove(logger);
+  logger = ii->logger_impl.Add(
+      [=](const LogMessage& msg) {
+        func(msg.level, msg.filename, msg.line, msg.message.c_str());
+      },
+      min_level, UINT_MAX);
+}
+
+NT_Logger AddLogger(NT_Inst inst,
+                    std::function<void(const LogMessage& msg)> func,
+                    unsigned int min_level, unsigned int max_level) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (!ii) return 0;
+
+  if (min_level < ii->logger.min_level()) ii->logger.set_min_level(min_level);
+
+  return Handle(i, ii->logger_impl.Add(func, min_level, max_level),
+                Handle::kLogger);
+}
+
+NT_LoggerPoller CreateLoggerPoller(NT_Inst inst) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (!ii) return 0;
+
+  return Handle(i, ii->logger_impl.CreatePoller(), Handle::kLoggerPoller);
+}
+
+void DestroyLoggerPoller(NT_LoggerPoller poller) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kLoggerPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  ii->logger_impl.RemovePoller(id);
+}
+
+NT_Logger AddPolledLogger(NT_LoggerPoller poller, unsigned int min_level,
+                          unsigned int max_level) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kLoggerPoller);
+  int i = handle.GetInst();
+  auto ii = InstanceImpl::Get(i);
+  if (id < 0 || !ii) return 0;
+
+  if (min_level < ii->logger.min_level()) ii->logger.set_min_level(min_level);
+
+  return Handle(i, ii->logger_impl.AddPolled(id, min_level, max_level),
+                Handle::kLogger);
+}
+
+std::vector<LogMessage> PollLogger(NT_LoggerPoller poller) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kLoggerPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return std::vector<LogMessage>{};
+
+  return ii->logger_impl.Poll(static_cast<unsigned int>(id));
+}
+
+std::vector<LogMessage> PollLogger(NT_LoggerPoller poller, double timeout,
+                                   bool* timed_out) {
+  *timed_out = false;
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kLoggerPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return std::vector<LogMessage>{};
+
+  return ii->logger_impl.Poll(static_cast<unsigned int>(id), timeout,
+                              timed_out);
+}
+
+void CancelPollLogger(NT_LoggerPoller poller) {
+  Handle handle{poller};
+  int id = handle.GetTypedIndex(Handle::kLoggerPoller);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (id < 0 || !ii) return;
+
+  ii->logger_impl.CancelPoll(id);
+}
+
+void RemoveLogger(NT_Logger logger) {
+  Handle handle{logger};
+  int uid = handle.GetTypedIndex(Handle::kLogger);
+  auto ii = InstanceImpl::Get(handle.GetInst());
+  if (uid < 0 || !ii) return;
+
+  ii->logger_impl.Remove(uid);
+  ii->logger.set_min_level(ii->logger_impl.GetMinLevel());
+}
+
+bool WaitForLoggerQueue(NT_Inst inst, double timeout) {
+  int i = Handle{inst}.GetTypedInst(Handle::kInstance);
+  auto ii = InstanceImpl::Get(i);
+  if (!ii) return true;
+  return ii->logger_impl.WaitForQueue(timeout);
+}
+
+}  // namespace nt
diff --git a/ntcore/src/main/native/cpp/ntcore_test.cpp b/ntcore/src/main/native/cpp/ntcore_test.cpp
new file mode 100644
index 0000000..f74172e
--- /dev/null
+++ b/ntcore/src/main/native/cpp/ntcore_test.cpp
@@ -0,0 +1,246 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "ntcore_test.h"
+
+#include <wpi/memory.h>
+
+#include "Value_internal.h"
+
+extern "C" {
+struct NT_String* NT_GetStringForTesting(const char* string, int* struct_size) {
+  struct NT_String* str =
+      static_cast<NT_String*>(wpi::CheckedCalloc(1, sizeof(NT_String)));
+  nt::ConvertToC(wpi::StringRef(string), str);
+  *struct_size = sizeof(NT_String);
+  return str;
+}
+
+struct NT_EntryInfo* NT_GetEntryInfoForTesting(const char* name,
+                                               enum NT_Type type,
+                                               unsigned int flags,
+                                               uint64_t last_change,
+                                               int* struct_size) {
+  struct NT_EntryInfo* entry_info =
+      static_cast<NT_EntryInfo*>(wpi::CheckedCalloc(1, sizeof(NT_EntryInfo)));
+  nt::ConvertToC(wpi::StringRef(name), &entry_info->name);
+  entry_info->type = type;
+  entry_info->flags = flags;
+  entry_info->last_change = last_change;
+  *struct_size = sizeof(NT_EntryInfo);
+  return entry_info;
+}
+
+void NT_FreeEntryInfoForTesting(struct NT_EntryInfo* info) {
+  std::free(info->name.str);
+  std::free(info);
+}
+
+struct NT_ConnectionInfo* NT_GetConnectionInfoForTesting(
+    const char* remote_id, const char* remote_ip, unsigned int remote_port,
+    uint64_t last_update, unsigned int protocol_version, int* struct_size) {
+  struct NT_ConnectionInfo* conn_info = static_cast<NT_ConnectionInfo*>(
+      wpi::CheckedCalloc(1, sizeof(NT_ConnectionInfo)));
+  nt::ConvertToC(wpi::StringRef(remote_id), &conn_info->remote_id);
+  nt::ConvertToC(wpi::StringRef(remote_ip), &conn_info->remote_ip);
+  conn_info->remote_port = remote_port;
+  conn_info->last_update = last_update;
+  conn_info->protocol_version = protocol_version;
+  *struct_size = sizeof(NT_ConnectionInfo);
+  return conn_info;
+}
+
+void NT_FreeConnectionInfoForTesting(struct NT_ConnectionInfo* info) {
+  std::free(info->remote_id.str);
+  std::free(info->remote_ip.str);
+  std::free(info);
+}
+
+struct NT_Value* NT_GetValueBooleanForTesting(uint64_t last_change, int val,
+                                              int* struct_size) {
+  struct NT_Value* value =
+      static_cast<NT_Value*>(wpi::CheckedCalloc(1, sizeof(NT_Value)));
+  value->type = NT_BOOLEAN;
+  value->last_change = last_change;
+  value->data.v_boolean = val;
+  *struct_size = sizeof(NT_Value);
+  return value;
+}
+
+struct NT_Value* NT_GetValueDoubleForTesting(uint64_t last_change, double val,
+                                             int* struct_size) {
+  struct NT_Value* value =
+      static_cast<NT_Value*>(wpi::CheckedCalloc(1, sizeof(NT_Value)));
+  value->type = NT_DOUBLE;
+  value->last_change = last_change;
+  value->data.v_double = val;
+  *struct_size = sizeof(NT_Value);
+  return value;
+}
+
+struct NT_Value* NT_GetValueStringForTesting(uint64_t last_change,
+                                             const char* str,
+                                             int* struct_size) {
+  struct NT_Value* value =
+      static_cast<NT_Value*>(wpi::CheckedCalloc(1, sizeof(NT_Value)));
+  value->type = NT_STRING;
+  value->last_change = last_change;
+  nt::ConvertToC(wpi::StringRef(str), &value->data.v_string);
+  *struct_size = sizeof(NT_Value);
+  return value;
+}
+
+struct NT_Value* NT_GetValueRawForTesting(uint64_t last_change, const char* raw,
+                                          int raw_len, int* struct_size) {
+  struct NT_Value* value =
+      static_cast<NT_Value*>(wpi::CheckedCalloc(1, sizeof(NT_Value)));
+  value->type = NT_RAW;
+  value->last_change = last_change;
+  nt::ConvertToC(wpi::StringRef(raw, raw_len), &value->data.v_string);
+  *struct_size = sizeof(NT_Value);
+  return value;
+}
+
+struct NT_Value* NT_GetValueBooleanArrayForTesting(uint64_t last_change,
+                                                   const int* arr,
+                                                   size_t array_len,
+                                                   int* struct_size) {
+  struct NT_Value* value =
+      static_cast<NT_Value*>(wpi::CheckedCalloc(1, sizeof(NT_Value)));
+  value->type = NT_BOOLEAN_ARRAY;
+  value->last_change = last_change;
+  value->data.arr_boolean.arr = NT_AllocateBooleanArray(array_len);
+  value->data.arr_boolean.size = array_len;
+  std::memcpy(value->data.arr_boolean.arr, arr,
+              value->data.arr_boolean.size * sizeof(int));
+  *struct_size = sizeof(NT_Value);
+  return value;
+}
+
+struct NT_Value* NT_GetValueDoubleArrayForTesting(uint64_t last_change,
+                                                  const double* arr,
+                                                  size_t array_len,
+                                                  int* struct_size) {
+  struct NT_Value* value =
+      static_cast<NT_Value*>(wpi::CheckedCalloc(1, sizeof(NT_Value)));
+  value->type = NT_BOOLEAN;
+  value->last_change = last_change;
+  value->data.arr_double.arr = NT_AllocateDoubleArray(array_len);
+  value->data.arr_double.size = array_len;
+  std::memcpy(value->data.arr_double.arr, arr,
+              value->data.arr_double.size * sizeof(int));
+  *struct_size = sizeof(NT_Value);
+  return value;
+}
+
+struct NT_Value* NT_GetValueStringArrayForTesting(uint64_t last_change,
+                                                  const struct NT_String* arr,
+                                                  size_t array_len,
+                                                  int* struct_size) {
+  struct NT_Value* value =
+      static_cast<NT_Value*>(wpi::CheckedCalloc(1, sizeof(NT_Value)));
+  value->type = NT_BOOLEAN;
+  value->last_change = last_change;
+  value->data.arr_string.arr = NT_AllocateStringArray(array_len);
+  value->data.arr_string.size = array_len;
+  for (size_t i = 0; i < value->data.arr_string.size; ++i) {
+    size_t len = arr[i].len;
+    value->data.arr_string.arr[i].len = len;
+    value->data.arr_string.arr[i].str =
+        static_cast<char*>(wpi::CheckedMalloc(len + 1));
+    std::memcpy(value->data.arr_string.arr[i].str, arr[i].str, len + 1);
+  }
+  *struct_size = sizeof(NT_Value);
+  return value;
+}
+// No need for free as one already exists in the main library
+
+static void CopyNtValue(const struct NT_Value* copy_from,
+                        struct NT_Value* copy_to) {
+  auto cpp_value = nt::ConvertFromC(*copy_from);
+  nt::ConvertToC(*cpp_value, copy_to);
+}
+
+static void CopyNtString(const struct NT_String* copy_from,
+                         struct NT_String* copy_to) {
+  nt::ConvertToC(wpi::StringRef(copy_from->str, copy_from->len), copy_to);
+}
+
+struct NT_RpcParamDef* NT_GetRpcParamDefForTesting(const char* name,
+                                                   const struct NT_Value* val,
+                                                   int* struct_size) {
+  struct NT_RpcParamDef* def = static_cast<NT_RpcParamDef*>(
+      wpi::CheckedCalloc(1, sizeof(NT_RpcParamDef)));
+  nt::ConvertToC(wpi::StringRef(name), &def->name);
+  CopyNtValue(val, &def->def_value);
+  *struct_size = sizeof(NT_RpcParamDef);
+  return def;
+}
+
+void NT_FreeRpcParamDefForTesting(struct NT_RpcParamDef* def) {
+  NT_DisposeValue(&def->def_value);
+  NT_DisposeString(&def->name);
+  std::free(def);
+}
+
+struct NT_RpcResultDef* NT_GetRpcResultsDefForTesting(const char* name,
+                                                      enum NT_Type type,
+                                                      int* struct_size) {
+  struct NT_RpcResultDef* def = static_cast<NT_RpcResultDef*>(
+      wpi::CheckedCalloc(1, sizeof(NT_RpcResultDef)));
+  nt::ConvertToC(wpi::StringRef(name), &def->name);
+  def->type = type;
+  *struct_size = sizeof(NT_RpcResultDef);
+  return def;
+}
+
+void NT_FreeRpcResultsDefForTesting(struct NT_RpcResultDef* def) {
+  NT_DisposeString(&def->name);
+  std::free(def);
+}
+
+struct NT_RpcDefinition* NT_GetRpcDefinitionForTesting(
+    unsigned int version, const char* name, size_t num_params,
+    const struct NT_RpcParamDef* params, size_t num_results,
+    const struct NT_RpcResultDef* results, int* struct_size) {
+  struct NT_RpcDefinition* def = static_cast<NT_RpcDefinition*>(
+      wpi::CheckedCalloc(1, sizeof(NT_RpcDefinition)));
+  def->version = version;
+  nt::ConvertToC(wpi::StringRef(name), &def->name);
+  def->num_params = num_params;
+  def->params = static_cast<NT_RpcParamDef*>(
+      wpi::CheckedMalloc(num_params * sizeof(NT_RpcParamDef)));
+  for (size_t i = 0; i < num_params; ++i) {
+    CopyNtString(&params[i].name, &def->params[i].name);
+    CopyNtValue(&params[i].def_value, &def->params[i].def_value);
+  }
+  def->num_results = num_results;
+  def->results = static_cast<NT_RpcResultDef*>(
+      wpi::CheckedMalloc(num_results * sizeof(NT_RpcResultDef)));
+  for (size_t i = 0; i < num_results; ++i) {
+    CopyNtString(&results[i].name, &def->results[i].name);
+    def->results[i].type = results[i].type;
+  }
+  *struct_size = sizeof(NT_RpcDefinition);
+  return def;
+}
+// No need for free as one already exists in the main library
+
+struct NT_RpcAnswer* NT_GetRpcAnswerForTesting(
+    unsigned int rpc_id, unsigned int call_uid, const char* name,
+    const char* params, size_t params_len, int* struct_size) {
+  struct NT_RpcAnswer* info =
+      static_cast<NT_RpcAnswer*>(wpi::CheckedCalloc(1, sizeof(NT_RpcAnswer)));
+  info->entry = rpc_id;
+  info->call = call_uid;
+  nt::ConvertToC(wpi::StringRef(name), &info->name);
+  nt::ConvertToC(wpi::StringRef(params, params_len), &info->params);
+  *struct_size = sizeof(NT_RpcAnswer);
+  return info;
+}
+// No need for free as one already exists in the main library
+}  // extern "C"
diff --git a/ntcore/src/main/native/cpp/tables/ITableListener.cpp b/ntcore/src/main/native/cpp/tables/ITableListener.cpp
new file mode 100644
index 0000000..6abd3bb
--- /dev/null
+++ b/ntcore/src/main/native/cpp/tables/ITableListener.cpp
@@ -0,0 +1,16 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "tables/ITableListener.h"
+
+#include "ntcore_c.h"
+
+void ITableListener::ValueChangedEx(ITable* source, wpi::StringRef key,
+                                    std::shared_ptr<nt::Value> value,
+                                    unsigned int flags) {
+  ValueChanged(source, key, value, (flags & NT_NOTIFY_NEW) != 0);
+}
diff --git a/ntcore/src/main/native/include/networktables/EntryListenerFlags.h b/ntcore/src/main/native/include/networktables/EntryListenerFlags.h
new file mode 100644
index 0000000..c5cc620
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/EntryListenerFlags.h
@@ -0,0 +1,82 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NETWORKTABLES_ENTRYLISTENERFLAGS_H_
+#define NTCORE_NETWORKTABLES_ENTRYLISTENERFLAGS_H_
+
+#include "ntcore_c.h"
+
+namespace nt {
+
+/** Entry listener flags */
+namespace EntryListenerFlags {
+
+/**
+ * Flag values for use with entry listeners.
+ *
+ * The flags are a bitmask and must be OR'ed together to indicate the
+ * combination of events desired to be received.
+ *
+ * The constants kNew, kDelete, kUpdate, and kFlags represent different events
+ * that can occur to entries.
+ *
+ * By default, notifications are only generated for remote changes occurring
+ * after the listener is created.  The constants kImmediate and kLocal are
+ * modifiers that cause notifications to be generated at other times.
+ *
+ * @ingroup ntcore_cpp_api
+ */
+enum {
+  /**
+   * Initial listener addition.
+   * Set this flag to receive immediate notification of entries matching the
+   * flag criteria (generally only useful when combined with kNew).
+   */
+  kImmediate = NT_NOTIFY_IMMEDIATE,
+
+  /**
+   * Changed locally.
+   * Set this flag to receive notification of both local changes and changes
+   * coming from remote nodes.  By default, notifications are only generated
+   * for remote changes.  Must be combined with some combination of kNew,
+   * kDelete, kUpdate, and kFlags to receive notifications of those respective
+   * events.
+   */
+  kLocal = NT_NOTIFY_LOCAL,
+
+  /**
+   * Newly created entry.
+   * Set this flag to receive a notification when an entry is created.
+   */
+  kNew = NT_NOTIFY_NEW,
+
+  /**
+   * Entry was deleted.
+   * Set this flag to receive a notification when an entry is deleted.
+   */
+  kDelete = NT_NOTIFY_DELETE,
+
+  /**
+   * Entry's value changed.
+   * Set this flag to receive a notification when an entry's value (or type)
+   * changes.
+   */
+  kUpdate = NT_NOTIFY_UPDATE,
+
+  /**
+   * Entry's flags changed.
+   * Set this flag to receive a notification when an entry's flags value
+   * changes.
+   */
+  kFlags = NT_NOTIFY_FLAGS
+};
+
+}  // namespace EntryListenerFlags
+
+}  // namespace nt
+
+#endif  // NTCORE_NETWORKTABLES_ENTRYLISTENERFLAGS_H_
diff --git a/ntcore/src/main/native/include/networktables/NetworkTable.h b/ntcore/src/main/native/include/networktables/NetworkTable.h
new file mode 100644
index 0000000..6bc2af6
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/NetworkTable.h
@@ -0,0 +1,777 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NETWORKTABLES_NETWORKTABLE_H_
+#define NTCORE_NETWORKTABLES_NETWORKTABLE_H_
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <wpi/ArrayRef.h>
+#include <wpi/StringMap.h>
+#include <wpi/Twine.h>
+#include <wpi/mutex.h>
+
+#include "networktables/NetworkTableEntry.h"
+#include "networktables/TableEntryListener.h"
+#include "networktables/TableListener.h"
+#include "ntcore_c.h"
+#include "tables/ITable.h"
+
+namespace nt {
+
+using wpi::ArrayRef;
+using wpi::StringRef;
+using wpi::Twine;
+
+class NetworkTableInstance;
+
+#ifdef __GNUC__
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
+/**
+ * @defgroup ntcore_cpp_api ntcore C++ object-oriented API
+ *
+ * Recommended interface for C++, identical to Java API.
+ */
+
+/**
+ * A network table that knows its subtable path.
+ * @ingroup ntcore_cpp_api
+ */
+class NetworkTable final : public ITable {
+ private:
+  NT_Inst m_inst;
+  std::string m_path;
+  mutable wpi::mutex m_mutex;
+  mutable wpi::StringMap<NT_Entry> m_entries;
+  typedef std::pair<ITableListener*, NT_EntryListener> Listener;
+  std::vector<Listener> m_listeners;
+  std::vector<NT_EntryListener> m_lambdaListeners;
+
+  static std::vector<std::string> s_ip_addresses;
+  static std::string s_persistent_filename;
+  static bool s_client;
+  static bool s_enable_ds;
+  static bool s_running;
+  static unsigned int s_port;
+
+  struct private_init {};
+  friend class NetworkTableInstance;
+
+ public:
+  /**
+   * Gets the "base name" of a key. For example, "/foo/bar" becomes "bar".
+   * If the key has a trailing slash, returns an empty string.
+   *
+   * @param key key
+   * @return base name
+   */
+  static StringRef BasenameKey(StringRef key);
+
+  /**
+   * Normalizes an network table key to contain no consecutive slashes and
+   * optionally start with a leading slash. For example:
+   *
+   * <pre><code>
+   * normalizeKey("/foo/bar", true)  == "/foo/bar"
+   * normalizeKey("foo/bar", true)   == "/foo/bar"
+   * normalizeKey("/foo/bar", false) == "foo/bar"
+   * normalizeKey("foo//bar", false) == "foo/bar"
+   * </code></pre>
+   *
+   * @param key              the key to normalize
+   * @param withLeadingSlash whether or not the normalized key should begin
+   *                         with a leading slash
+   * @return normalized key
+   */
+  static std::string NormalizeKey(const Twine& key,
+                                  bool withLeadingSlash = true);
+
+  static StringRef NormalizeKey(const Twine& key,
+                                wpi::SmallVectorImpl<char>& buf,
+                                bool withLeadingSlash = true);
+
+  /**
+   * Gets a list of the names of all the super tables of a given key. For
+   * example, the key "/foo/bar/baz" has a hierarchy of "/", "/foo",
+   * "/foo/bar", and "/foo/bar/baz".
+   *
+   * @param key the key
+   * @return List of super tables
+   */
+  static std::vector<std::string> GetHierarchy(const Twine& key);
+
+  /**
+   * Constructor.  Use NetworkTableInstance::GetTable() or GetSubTable()
+   * instead.
+   */
+  NetworkTable(NT_Inst inst, const Twine& path, const private_init&);
+  virtual ~NetworkTable();
+
+  /**
+   * Gets the instance for the table.
+   *
+   * @return Instance
+   */
+  NetworkTableInstance GetInstance() const;
+
+  /**
+   * The path separator for sub-tables and keys
+   */
+  static const char PATH_SEPARATOR_CHAR;
+
+  /**
+   * Initializes network tables
+   */
+  WPI_DEPRECATED(
+      "use NetworkTableInstance::StartServer() or "
+      "NetworkTableInstance::StartClient() instead")
+  static void Initialize();
+
+  /**
+   * Shuts down network tables
+   */
+  WPI_DEPRECATED(
+      "use NetworkTableInstance::StopServer() or "
+      "NetworkTableInstance::StopClient() instead")
+  static void Shutdown();
+
+  /**
+   * set that network tables should be a client
+   * This must be called before initialize or GetTable
+   */
+  WPI_DEPRECATED("use NetworkTableInstance::StartClient() instead")
+  static void SetClientMode();
+
+  /**
+   * set that network tables should be a server
+   * This must be called before initialize or GetTable
+   */
+  WPI_DEPRECATED("use NetworkTableInstance::StartServer() instead")
+  static void SetServerMode();
+
+  /**
+   * set the team the robot is configured for (this will set the mdns address
+   * that network tables will connect to in client mode)
+   * This must be called before initialize or GetTable
+   *
+   * @param team the team number
+   */
+  WPI_DEPRECATED(
+      "use NetworkTableInstance::SetServerTeam() or "
+      "NetworkTableInstance::StartClientTeam() instead")
+  static void SetTeam(int team);
+
+  /**
+   * @param address the adress that network tables will connect to in client
+   * mode
+   */
+  WPI_DEPRECATED(
+      "use NetworkTableInstance::SetServer() or "
+      "NetworkTableInstance::StartClient() instead")
+  static void SetIPAddress(StringRef address);
+
+  /**
+   * @param addresses the addresses that network tables will connect to in
+   * client mode (in round robin order)
+   */
+  WPI_DEPRECATED(
+      "use NetworkTableInstance::SetServer() or "
+      "NetworkTableInstance::StartClient() instead")
+  static void SetIPAddress(ArrayRef<std::string> addresses);
+
+  /**
+   * Set the port number that network tables will connect to in client
+   * mode or listen to in server mode.
+   *
+   * @param port the port number
+   */
+  WPI_DEPRECATED(
+      "use the appropriate parameters to NetworkTableInstance::SetServer(), "
+      "NetworkTableInstance::StartClient(), "
+      "NetworkTableInstance::StartServer(), and "
+      "NetworkTableInstance::StartDSClient() instead")
+  static void SetPort(unsigned int port);
+
+  /**
+   * Enable requesting the server address from the Driver Station.
+   *
+   * @param enabled whether to enable the connection to the local DS
+   */
+  WPI_DEPRECATED(
+      "use NetworkTableInstance::StartDSClient() and "
+      "NetworkTableInstance::StopDSClient() instead")
+  static void SetDSClientEnabled(bool enabled);
+
+  /**
+   * Sets the persistent filename.
+   *
+   * @param filename the filename that the network tables server uses for
+   * automatic loading and saving of persistent values
+   */
+  WPI_DEPRECATED(
+      "use the appropriate parameter to NetworkTableInstance::StartServer() "
+      "instead")
+  static void SetPersistentFilename(StringRef filename);
+
+  /**
+   * Sets the network identity.
+   * This is provided in the connection info on the remote end.
+   *
+   * @param name identity
+   */
+  WPI_DEPRECATED("use NetworkTableInstance::SetNetworkIdentity() instead")
+  static void SetNetworkIdentity(StringRef name);
+
+  /**
+   * Deletes ALL keys in ALL subtables.  Use with caution!
+   */
+  WPI_DEPRECATED("use NetworkTableInstance::DeleteAllEntries() instead")
+  static void GlobalDeleteAll();
+
+  /**
+   * Flushes all updated values immediately to the network.
+   * Note: This is rate-limited to protect the network from flooding.
+   * This is primarily useful for synchronizing network updates with
+   * user code.
+   */
+  WPI_DEPRECATED("use NetworkTableInstance::Flush() instead")
+  static void Flush();
+
+  /**
+   * Set the periodic update rate.
+   * Sets how frequently updates are sent to other nodes over the network.
+   *
+   * @param interval update interval in seconds (range 0.01 to 1.0)
+   */
+  WPI_DEPRECATED("use NetworkTableInstance::SetUpdateRate() instead")
+  static void SetUpdateRate(double interval);
+
+  /**
+   * Saves persistent keys to a file.  The server does this automatically.
+   *
+   * @param filename file name
+   * @return Error (or nullptr).
+   */
+  WPI_DEPRECATED("use NetworkTableInstance::SavePersistent() instead")
+  static const char* SavePersistent(StringRef filename);
+
+  /**
+   * Loads persistent keys from a file.  The server does this automatically.
+   *
+   * @param filename file name
+   * @param warn callback function called for warnings
+   * @return Error (or nullptr).
+   */
+  WPI_DEPRECATED("use NetworkTableInstance::LoadPersistent() instead")
+  static const char* LoadPersistent(
+      StringRef filename,
+      std::function<void(size_t line, const char* msg)> warn);
+
+  /**
+   * Gets the table with the specified key. If the table does not exist, a new
+   * table will be created.<br>
+   * This will automatically initialize network tables if it has not been
+   * already.
+   *
+   * @param key  the key name
+   * @return the network table requested
+   */
+  WPI_DEPRECATED(
+      "use NetworkTableInstance::GetTable() or "
+      "NetworkTableInstance::GetEntry() instead")
+  static std::shared_ptr<NetworkTable> GetTable(StringRef key);
+
+  /**
+   * Gets the entry for a subkey.
+   *
+   * @param key the key name
+   * @return Network table entry.
+   */
+  NetworkTableEntry GetEntry(const Twine& key) const;
+
+  /**
+   * Listen to keys only within this table.
+   *
+   * @param listener    listener to add
+   * @param flags       EntryListenerFlags bitmask
+   * @return Listener handle
+   */
+  NT_EntryListener AddEntryListener(TableEntryListener listener,
+                                    unsigned int flags) const;
+
+  /**
+   * Listen to a single key.
+   *
+   * @param key         the key name
+   * @param listener    listener to add
+   * @param flags       EntryListenerFlags bitmask
+   * @return Listener handle
+   */
+  NT_EntryListener AddEntryListener(const Twine& key,
+                                    TableEntryListener listener,
+                                    unsigned int flags) const;
+
+  /**
+   * Remove an entry listener.
+   *
+   * @param listener    listener handle
+   */
+  void RemoveEntryListener(NT_EntryListener listener) const;
+
+  /**
+   * Listen for sub-table creation.
+   * This calls the listener once for each newly created sub-table.
+   * It immediately calls the listener for any existing sub-tables.
+   *
+   * @param listener        listener to add
+   * @param localNotify     notify local changes as well as remote
+   * @return Listener handle
+   */
+  NT_EntryListener AddSubTableListener(TableListener listener,
+                                       bool localNotify = false);
+
+  /**
+   * Remove a sub-table listener.
+   *
+   * @param listener    listener handle
+   */
+  void RemoveTableListener(NT_EntryListener listener);
+
+  WPI_DEPRECATED(
+      "use AddEntryListener() instead with flags value of NT_NOTIFY_NEW | "
+      "NT_NOTIFY_UPDATE")
+  void AddTableListener(ITableListener* listener) override;
+
+  WPI_DEPRECATED(
+      "use AddEntryListener() instead with flags value of NT_NOTIFY_NEW | "
+      "NT_NOTIFY_UPDATE | NT_NOTIFY_IMMEDIATE")
+  void AddTableListener(ITableListener* listener,
+                        bool immediateNotify) override;
+
+  WPI_DEPRECATED("use AddEntryListener() instead")
+  void AddTableListenerEx(ITableListener* listener,
+                          unsigned int flags) override;
+
+  WPI_DEPRECATED("use AddEntryListener() instead")
+  void AddTableListener(StringRef key, ITableListener* listener,
+                        bool immediateNotify) override;
+
+  WPI_DEPRECATED("use AddEntryListener() instead")
+  void AddTableListenerEx(StringRef key, ITableListener* listener,
+                          unsigned int flags) override;
+
+  WPI_DEPRECATED("use AddSubTableListener(TableListener, bool) instead")
+  void AddSubTableListener(ITableListener* listener) override;
+
+  WPI_DEPRECATED("use AddSubTableListener(TableListener, bool) instead")
+  void AddSubTableListener(ITableListener* listener, bool localNotify) override;
+
+  WPI_DEPRECATED("use RemoveTableListener(NT_EntryListener) instead")
+  void RemoveTableListener(ITableListener* listener) override;
+
+  /**
+   * Returns the table at the specified key. If there is no table at the
+   * specified key, it will create a new table
+   *
+   * @param key the key name
+   * @return the networktable to be returned
+   */
+  std::shared_ptr<NetworkTable> GetSubTable(const Twine& key) const override;
+
+  /**
+   * Determines whether the given key is in this table.
+   *
+   * @param key the key to search for
+   * @return true if the table as a value assigned to the given key
+   */
+  bool ContainsKey(const Twine& key) const override;
+
+  /**
+   * Determines whether there exists a non-empty subtable for this key
+   * in this table.
+   *
+   * @param key the key to search for
+   * @return true if there is a subtable with the key which contains at least
+   * one key/subtable of its own
+   */
+  bool ContainsSubTable(const Twine& key) const override;
+
+  /**
+   * Gets all keys in the table (not including sub-tables).
+   *
+   * @param types bitmask of types; 0 is treated as a "don't care".
+   * @return keys currently in the table
+   */
+  std::vector<std::string> GetKeys(int types = 0) const override;
+
+  /**
+   * Gets the names of all subtables in the table.
+   *
+   * @return subtables currently in the table
+   */
+  std::vector<std::string> GetSubTables() const override;
+
+  /**
+   * Makes a key's value persistent through program restarts.
+   *
+   * @param key the key to make persistent
+   */
+  void SetPersistent(StringRef key) override;
+
+  /**
+   * Stop making a key's value persistent through program restarts.
+   * The key cannot be null.
+   *
+   * @param key the key name
+   */
+  void ClearPersistent(StringRef key) override;
+
+  /**
+   * Returns whether the value is persistent through program restarts.
+   * The key cannot be null.
+   *
+   * @param key the key name
+   */
+  bool IsPersistent(StringRef key) const override;
+
+  /**
+   * Sets flags on the specified key in this table. The key can
+   * not be null.
+   *
+   * @param key the key name
+   * @param flags the flags to set (bitmask)
+   */
+  void SetFlags(StringRef key, unsigned int flags) override;
+
+  /**
+   * Clears flags on the specified key in this table. The key can
+   * not be null.
+   *
+   * @param key the key name
+   * @param flags the flags to clear (bitmask)
+   */
+  void ClearFlags(StringRef key, unsigned int flags) override;
+
+  /**
+   * Returns the flags for the specified key.
+   *
+   * @param key the key name
+   * @return the flags, or 0 if the key is not defined
+   */
+  unsigned int GetFlags(StringRef key) const override;
+
+  /**
+   * Deletes the specified key in this table.
+   *
+   * @param key the key name
+   */
+  void Delete(const Twine& key) override;
+
+  /**
+   * Put a number in the table
+   *
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  bool PutNumber(StringRef key, double value) override;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   *
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  bool SetDefaultNumber(StringRef key, double defaultValue) override;
+
+  /**
+   * Gets the number associated with the given name.
+   *
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  double GetNumber(StringRef key, double defaultValue) const override;
+
+  /**
+   * Put a string in the table
+   *
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  bool PutString(StringRef key, StringRef value) override;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   *
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  bool SetDefaultString(StringRef key, StringRef defaultValue) override;
+
+  /**
+   * Gets the string associated with the given name. If the key does not
+   * exist or is of different type, it will return the default value.
+   *
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  std::string GetString(StringRef key, StringRef defaultValue) const override;
+
+  /**
+   * Put a boolean in the table
+   *
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  bool PutBoolean(StringRef key, bool value) override;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   *
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  bool SetDefaultBoolean(StringRef key, bool defaultValue) override;
+
+  /**
+   * Gets the boolean associated with the given name. If the key does not
+   * exist or is of different type, it will return the default value.
+   *
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  bool GetBoolean(StringRef key, bool defaultValue) const override;
+
+  /**
+   * Put a boolean array in the table
+   *
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   *
+   * @note The array must be of int's rather than of bool's because
+   *       std::vector<bool> is special-cased in C++.  0 is false, any
+   *       non-zero value is true.
+   */
+  bool PutBooleanArray(StringRef key, ArrayRef<int> value) override;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   *
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @return False if the table key exists with a different type
+   */
+  bool SetDefaultBooleanArray(StringRef key,
+                              ArrayRef<int> defaultValue) override;
+
+  /**
+   * Returns the boolean array the key maps to. If the key does not exist or is
+   * of different type, it will return the default value.
+   *
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   *
+   * @note This makes a copy of the array.  If the overhead of this is a
+   *       concern, use GetValue() instead.
+   *
+   * @note The returned array is std::vector<int> instead of std::vector<bool>
+   *       because std::vector<bool> is special-cased in C++.  0 is false, any
+   *       non-zero value is true.
+   */
+  std::vector<int> GetBooleanArray(StringRef key,
+                                   ArrayRef<int> defaultValue) const override;
+
+  /**
+   * Put a number array in the table
+   *
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  bool PutNumberArray(StringRef key, ArrayRef<double> value) override;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   *
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  bool SetDefaultNumberArray(StringRef key,
+                             ArrayRef<double> defaultValue) override;
+
+  /**
+   * Returns the number array the key maps to. If the key does not exist or is
+   * of different type, it will return the default value.
+   *
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   *
+   * @note This makes a copy of the array.  If the overhead of this is a
+   *       concern, use GetValue() instead.
+   */
+  std::vector<double> GetNumberArray(
+      StringRef key, ArrayRef<double> defaultValue) const override;
+
+  /**
+   * Put a string array in the table
+   *
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  bool PutStringArray(StringRef key, ArrayRef<std::string> value) override;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   *
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  bool SetDefaultStringArray(StringRef key,
+                             ArrayRef<std::string> defaultValue) override;
+
+  /**
+   * Returns the string array the key maps to. If the key does not exist or is
+   * of different type, it will return the default value.
+   *
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   *
+   * @note This makes a copy of the array.  If the overhead of this is a
+   *       concern, use GetValue() instead.
+   */
+  std::vector<std::string> GetStringArray(
+      StringRef key, ArrayRef<std::string> defaultValue) const override;
+
+  /**
+   * Put a raw value (byte array) in the table
+   *
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  bool PutRaw(StringRef key, StringRef value) override;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   *
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @return False if the table key exists with a different type
+   */
+  bool SetDefaultRaw(StringRef key, StringRef defaultValue) override;
+
+  /**
+   * Returns the raw value (byte array) the key maps to. If the key does not
+   * exist or is of different type, it will return the default value.
+   *
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   *
+   * @note This makes a copy of the raw contents.  If the overhead of this is a
+   *       concern, use GetValue() instead.
+   */
+  std::string GetRaw(StringRef key, StringRef defaultValue) const override;
+
+  /**
+   * Put a value in the table
+   *
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  bool PutValue(const Twine& key, std::shared_ptr<Value> value) override;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   *
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @return False if the table key exists with a different type
+   */
+  bool SetDefaultValue(const Twine& key,
+                       std::shared_ptr<Value> defaultValue) override;
+
+  /**
+   * Gets the value associated with a key as an object
+   *
+   * @param key the key of the value to look up
+   * @return the value associated with the given key, or nullptr if the key
+   * does not exist
+   */
+  std::shared_ptr<Value> GetValue(const Twine& key) const override;
+
+  /**
+   * Gets the full path of this table.  Does not include the trailing "/".
+   *
+   * @return The path (e.g "", "/foo").
+   */
+  StringRef GetPath() const override;
+
+  /**
+   * Save table values to a file.  The file format used is identical to
+   * that used for SavePersistent.
+   *
+   * @param filename  filename
+   * @return error string, or nullptr if successful
+   */
+  const char* SaveEntries(const Twine& filename) const;
+
+  /**
+   * Load table values from a file.  The file format used is identical to
+   * that used for SavePersistent / LoadPersistent.
+   *
+   * @param filename  filename
+   * @param warn      callback function for warnings
+   * @return error string, or nullptr if successful
+   */
+  const char* LoadEntries(
+      const Twine& filename,
+      std::function<void(size_t line, const char* msg)> warn);
+};
+
+#ifdef __GNUC__
+#pragma GCC diagnostic pop
+#endif
+
+}  // namespace nt
+
+// For backwards compatability
+#ifndef NAMESPACED_NT
+using nt::NetworkTable;  // NOLINT
+#endif
+
+#endif  // NTCORE_NETWORKTABLES_NETWORKTABLE_H_
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableEntry.h b/ntcore/src/main/native/include/networktables/NetworkTableEntry.h
new file mode 100644
index 0000000..16b5c61
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/NetworkTableEntry.h
@@ -0,0 +1,500 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NETWORKTABLES_NETWORKTABLEENTRY_H_
+#define NTCORE_NETWORKTABLES_NETWORKTABLEENTRY_H_
+
+#include <stdint.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <wpi/StringRef.h>
+#include <wpi/Twine.h>
+
+#include "networktables/NetworkTableType.h"
+#include "networktables/NetworkTableValue.h"
+#include "networktables/RpcCall.h"
+#include "ntcore_c.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+using wpi::ArrayRef;
+using wpi::StringRef;
+using wpi::Twine;
+
+class NetworkTableInstance;
+
+/**
+ * NetworkTables Entry
+ * @ingroup ntcore_cpp_api
+ */
+class NetworkTableEntry final {
+ public:
+  /**
+   * Flag values (as returned by GetFlags()).
+   */
+  enum Flags { kPersistent = NT_PERSISTENT };
+
+  /**
+   * Construct invalid instance.
+   */
+  NetworkTableEntry();
+
+  /**
+   * Construct from native handle.
+   *
+   * @param handle Native handle
+   */
+  explicit NetworkTableEntry(NT_Entry handle);
+
+  /**
+   * Determines if the native handle is valid.
+   *
+   * @return True if the native handle is valid, false otherwise.
+   */
+  explicit operator bool() const { return m_handle != 0; }
+
+  /**
+   * Gets the native handle for the entry.
+   *
+   * @return Native handle
+   */
+  NT_Entry GetHandle() const;
+
+  /**
+   * Gets the instance for the entry.
+   *
+   * @return Instance
+   */
+  NetworkTableInstance GetInstance() const;
+
+  /**
+   * Determines if the entry currently exists.
+   *
+   * @return True if the entry exists, false otherwise.
+   */
+  bool Exists() const;
+
+  /**
+   * Gets the name of the entry (the key).
+   *
+   * @return the entry's name
+   */
+  std::string GetName() const;
+
+  /**
+   * Gets the type of the entry.
+   *
+   * @return the entry's type
+   */
+  NetworkTableType GetType() const;
+
+  /**
+   * Returns the flags.
+   *
+   * @return the flags (bitmask)
+   */
+  unsigned int GetFlags() const;
+
+  /**
+   * Gets the last time the entry's value was changed.
+   *
+   * @return Entry last change time
+   */
+  uint64_t GetLastChange() const;
+
+  /**
+   * Gets combined information about the entry.
+   *
+   * @return Entry information
+   */
+  EntryInfo GetInfo() const;
+
+  /**
+   * Gets the entry's value. If the entry does not exist, returns nullptr.
+   *
+   * @return the entry's value or nullptr if it does not exist.
+   */
+  std::shared_ptr<Value> GetValue() const;
+
+  /**
+   * Gets the entry's value as a boolean. If the entry does not exist or is of
+   * different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  bool GetBoolean(bool defaultValue) const;
+
+  /**
+   * Gets the entry's value as a double. If the entry does not exist or is of
+   * different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  double GetDouble(double defaultValue) const;
+
+  /**
+   * Gets the entry's value as a string. If the entry does not exist or is of
+   * different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  std::string GetString(StringRef defaultValue) const;
+
+  /**
+   * Gets the entry's value as a raw. If the entry does not exist or is of
+   * different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   */
+  std::string GetRaw(StringRef defaultValue) const;
+
+  /**
+   * Gets the entry's value as a boolean array. If the entry does not exist
+   * or is of different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   *
+   * @note This makes a copy of the array.  If the overhead of this is a
+   *       concern, use GetValue() instead.
+   *
+   * @note The returned array is std::vector<int> instead of std::vector<bool>
+   *       because std::vector<bool> is special-cased in C++.  0 is false, any
+   *       non-zero value is true.
+   */
+  std::vector<int> GetBooleanArray(ArrayRef<int> defaultValue) const;
+
+  /**
+   * Gets the entry's value as a double array. If the entry does not exist
+   * or is of different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   *
+   * @note This makes a copy of the array.  If the overhead of this is a
+   *       concern, use GetValue() instead.
+   */
+  std::vector<double> GetDoubleArray(ArrayRef<double> defaultValue) const;
+
+  /**
+   * Gets the entry's value as a string array. If the entry does not exist
+   * or is of different type, it will return the default value.
+   *
+   * @param defaultValue the value to be returned if no value is found
+   * @return the entry's value or the given default value
+   *
+   * @note This makes a copy of the array.  If the overhead of this is a
+   *       concern, use GetValue() instead.
+   */
+  std::vector<std::string> GetStringArray(
+      ArrayRef<std::string> defaultValue) const;
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetDefaultValue(std::shared_ptr<Value> value);
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetDefaultBoolean(bool defaultValue);
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetDefaultDouble(double defaultValue);
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetDefaultString(const Twine& defaultValue);
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetDefaultRaw(StringRef defaultValue);
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetDefaultBooleanArray(ArrayRef<int> defaultValue);
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetDefaultDoubleArray(ArrayRef<double> defaultValue);
+
+  /**
+   * Sets the entry's value if it does not exist.
+   *
+   * @param defaultValue the default value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetDefaultStringArray(ArrayRef<std::string> defaultValue);
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetValue(std::shared_ptr<Value> value);
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetBoolean(bool value);
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetDouble(double value);
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetString(const Twine& value);
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetRaw(StringRef value);
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetBooleanArray(ArrayRef<int> value);
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetDoubleArray(ArrayRef<double> value);
+
+  /**
+   * Sets the entry's value.
+   *
+   * @param value the value to set
+   * @return False if the entry exists with a different type
+   */
+  bool SetStringArray(ArrayRef<std::string> value);
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  void ForceSetValue(std::shared_ptr<Value> value);
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  void ForceSetBoolean(bool value);
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  void ForceSetDouble(double value);
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  void ForceSetString(const Twine& value);
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  void ForceSetRaw(StringRef value);
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  void ForceSetBooleanArray(ArrayRef<int> value);
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  void ForceSetDoubleArray(ArrayRef<double> value);
+
+  /**
+   * Sets the entry's value.  If the value is of different type, the type is
+   * changed to match the new value.
+   *
+   * @param value the value to set
+   */
+  void ForceSetStringArray(ArrayRef<std::string> value);
+
+  /**
+   * Sets flags.
+   *
+   * @param flags the flags to set (bitmask)
+   */
+  void SetFlags(unsigned int flags);
+
+  /**
+   * Clears flags.
+   *
+   * @param flags the flags to clear (bitmask)
+   */
+  void ClearFlags(unsigned int flags);
+
+  /**
+   * Make value persistent through program restarts.
+   */
+  void SetPersistent();
+
+  /**
+   * Stop making value persistent through program restarts.
+   */
+  void ClearPersistent();
+
+  /**
+   * Returns whether the value is persistent through program restarts.
+   *
+   * @return True if the value is persistent.
+   */
+  bool IsPersistent() const;
+
+  /**
+   * Deletes the entry.
+   */
+  void Delete();
+
+  /**
+   * Create a callback-based RPC entry point.  Only valid to use on the server.
+   * The callback function will be called when the RPC is called.
+   * This function creates RPC version 0 definitions (raw data in and out).
+   *
+   * @param callback  callback function
+   */
+  void CreateRpc(std::function<void(const RpcAnswer& answer)> callback);
+
+  /**
+   * Create a polled RPC entry point.  Only valid to use on the server.
+   * The caller is responsible for calling NetworkTableInstance::PollRpc()
+   * to poll for servicing incoming RPC calls.
+   * This function creates RPC version 0 definitions (raw data in and out).
+   */
+  void CreatePolledRpc();
+
+  /**
+   * Call a RPC function.  May be used on either the client or server.
+   * This function is non-blocking.  Either RpcCall::GetResult() or
+   * RpcCall::CancelResult() must be called on the return value to either
+   * get or ignore the result of the call.
+   *
+   * @param params      parameter
+   * @return RPC call object.
+   */
+  RpcCall CallRpc(StringRef params);
+
+  /**
+   * Add a listener for changes to this entry.
+   *
+   * @param callback          listener to add
+   * @param flags             NotifyKind bitmask
+   * @return Listener handle
+   */
+  NT_EntryListener AddListener(
+      std::function<void(const EntryNotification& event)> callback,
+      unsigned int flags) const;
+
+  /**
+   * Remove an entry listener.
+   *
+   * @param entry_listener Listener handle to remove
+   */
+  void RemoveListener(NT_EntryListener entry_listener);
+
+  /**
+   * Equality operator.  Returns true if both instances refer to the same
+   * native handle.
+   */
+  bool operator==(const NetworkTableEntry& oth) const {
+    return m_handle == oth.m_handle;
+  }
+
+  /** Inequality operator. */
+  bool operator!=(const NetworkTableEntry& oth) const {
+    return !(*this == oth);
+  }
+
+ protected:
+  /* Native handle */
+  NT_Entry m_handle;
+};
+
+}  // namespace nt
+
+#include "networktables/NetworkTableEntry.inl"
+
+#endif  // NTCORE_NETWORKTABLES_NETWORKTABLEENTRY_H_
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableEntry.inl b/ntcore/src/main/native/include/networktables/NetworkTableEntry.inl
new file mode 100644
index 0000000..f95b1a8
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/NetworkTableEntry.inl
@@ -0,0 +1,232 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) FIRST 2017. All Rights Reserved.                             */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NT_ENTRY_INL_
+#define NT_ENTRY_INL_
+
+namespace nt {
+
+inline NetworkTableEntry::NetworkTableEntry() : m_handle{0} {}
+
+inline NetworkTableEntry::NetworkTableEntry(NT_Entry handle)
+    : m_handle{handle} {}
+
+inline NT_Entry NetworkTableEntry::GetHandle() const { return m_handle; }
+
+inline bool NetworkTableEntry::Exists() const {
+  return GetEntryType(m_handle) != NT_UNASSIGNED;
+}
+
+inline std::string NetworkTableEntry::GetName() const {
+  return GetEntryName(m_handle);
+}
+
+inline NetworkTableType NetworkTableEntry::GetType() const {
+  return static_cast<NetworkTableType>(GetEntryType(m_handle));
+}
+
+inline unsigned int NetworkTableEntry::GetFlags() const {
+  return GetEntryFlags(m_handle);
+}
+
+inline uint64_t NetworkTableEntry::GetLastChange() const {
+  return GetEntryLastChange(m_handle);
+}
+
+inline EntryInfo NetworkTableEntry::GetInfo() const {
+  return GetEntryInfo(m_handle);
+}
+
+inline std::shared_ptr<Value> NetworkTableEntry::GetValue() const {
+  return GetEntryValue(m_handle);
+}
+
+inline bool NetworkTableEntry::GetBoolean(bool defaultValue) const {
+  auto value = GetEntryValue(m_handle);
+  if (!value || value->type() != NT_BOOLEAN) return defaultValue;
+  return value->GetBoolean();
+}
+
+inline double NetworkTableEntry::GetDouble(double defaultValue) const {
+  auto value = GetEntryValue(m_handle);
+  if (!value || value->type() != NT_DOUBLE) return defaultValue;
+  return value->GetDouble();
+}
+
+inline std::string NetworkTableEntry::GetString(StringRef defaultValue) const {
+  auto value = GetEntryValue(m_handle);
+  if (!value || value->type() != NT_STRING) return defaultValue;
+  return value->GetString();
+}
+
+inline std::string NetworkTableEntry::GetRaw(StringRef defaultValue) const {
+  auto value = GetEntryValue(m_handle);
+  if (!value || value->type() != NT_RAW) return defaultValue;
+  return value->GetString();
+}
+
+inline std::vector<int> NetworkTableEntry::GetBooleanArray(
+    ArrayRef<int> defaultValue) const {
+  auto value = GetEntryValue(m_handle);
+  if (!value || value->type() != NT_BOOLEAN_ARRAY) return defaultValue;
+  return value->GetBooleanArray();
+}
+
+inline std::vector<double> NetworkTableEntry::GetDoubleArray(
+    ArrayRef<double> defaultValue) const {
+  auto value = GetEntryValue(m_handle);
+  if (!value || value->type() != NT_DOUBLE_ARRAY) return defaultValue;
+  return value->GetDoubleArray();
+}
+
+inline std::vector<std::string> NetworkTableEntry::GetStringArray(
+    ArrayRef<std::string> defaultValue) const {
+  auto value = GetEntryValue(m_handle);
+  if (!value || value->type() != NT_STRING_ARRAY) return defaultValue;
+  return value->GetStringArray();
+}
+
+inline bool NetworkTableEntry::SetDefaultValue(std::shared_ptr<Value> value) {
+  return SetDefaultEntryValue(m_handle, value);
+}
+
+inline bool NetworkTableEntry::SetDefaultBoolean(bool defaultValue) {
+  return SetDefaultEntryValue(m_handle, Value::MakeBoolean(defaultValue));
+}
+
+inline bool NetworkTableEntry::SetDefaultDouble(double defaultValue) {
+  return SetDefaultEntryValue(m_handle, Value::MakeDouble(defaultValue));
+}
+
+inline bool NetworkTableEntry::SetDefaultString(const Twine& defaultValue) {
+  return SetDefaultEntryValue(m_handle, Value::MakeString(defaultValue));
+}
+
+inline bool NetworkTableEntry::SetDefaultRaw(StringRef defaultValue) {
+  return SetDefaultEntryValue(m_handle, Value::MakeRaw(defaultValue));
+}
+
+inline bool NetworkTableEntry::SetDefaultBooleanArray(
+    ArrayRef<int> defaultValue) {
+  return SetDefaultEntryValue(m_handle, Value::MakeBooleanArray(defaultValue));
+}
+
+inline bool NetworkTableEntry::SetDefaultDoubleArray(
+    ArrayRef<double> defaultValue) {
+  return SetDefaultEntryValue(m_handle, Value::MakeDoubleArray(defaultValue));
+}
+
+inline bool NetworkTableEntry::SetDefaultStringArray(
+    ArrayRef<std::string> defaultValue) {
+  return SetDefaultEntryValue(m_handle, Value::MakeStringArray(defaultValue));
+}
+
+inline bool NetworkTableEntry::SetValue(std::shared_ptr<Value> value) {
+  return SetEntryValue(m_handle, value);
+}
+
+inline bool NetworkTableEntry::SetBoolean(bool value) {
+  return SetEntryValue(m_handle, Value::MakeBoolean(value));
+}
+
+inline bool NetworkTableEntry::SetDouble(double value) {
+  return SetEntryValue(m_handle, Value::MakeDouble(value));
+}
+
+inline bool NetworkTableEntry::SetString(const Twine& value) {
+  return SetEntryValue(m_handle, Value::MakeString(value));
+}
+
+inline bool NetworkTableEntry::SetRaw(StringRef value) {
+  return SetEntryValue(m_handle, Value::MakeRaw(value));
+}
+
+inline bool NetworkTableEntry::SetBooleanArray(ArrayRef<int> value) {
+  return SetEntryValue(m_handle, Value::MakeBooleanArray(value));
+}
+
+inline bool NetworkTableEntry::SetDoubleArray(ArrayRef<double> value) {
+  return SetEntryValue(m_handle, Value::MakeDoubleArray(value));
+}
+
+inline bool NetworkTableEntry::SetStringArray(ArrayRef<std::string> value) {
+  return SetEntryValue(m_handle, Value::MakeStringArray(value));
+}
+
+inline void NetworkTableEntry::ForceSetValue(std::shared_ptr<Value> value) {
+  SetEntryTypeValue(m_handle, value);
+}
+
+inline void NetworkTableEntry::ForceSetBoolean(bool value) {
+  SetEntryTypeValue(m_handle, Value::MakeBoolean(value));
+}
+
+inline void NetworkTableEntry::ForceSetDouble(double value) {
+  SetEntryTypeValue(m_handle, Value::MakeDouble(value));
+}
+
+inline void NetworkTableEntry::ForceSetString(const Twine& value) {
+  SetEntryTypeValue(m_handle, Value::MakeString(value));
+}
+
+inline void NetworkTableEntry::ForceSetRaw(StringRef value) {
+  SetEntryTypeValue(m_handle, Value::MakeRaw(value));
+}
+
+inline void NetworkTableEntry::ForceSetBooleanArray(ArrayRef<int> value) {
+  SetEntryTypeValue(m_handle, Value::MakeBooleanArray(value));
+}
+
+inline void NetworkTableEntry::ForceSetDoubleArray(ArrayRef<double> value) {
+  SetEntryTypeValue(m_handle, Value::MakeDoubleArray(value));
+}
+
+inline void NetworkTableEntry::ForceSetStringArray(
+    ArrayRef<std::string> value) {
+  SetEntryTypeValue(m_handle, Value::MakeStringArray(value));
+}
+
+inline void NetworkTableEntry::SetFlags(unsigned int flags) {
+  SetEntryFlags(m_handle, GetFlags() | flags);
+}
+
+inline void NetworkTableEntry::ClearFlags(unsigned int flags) {
+  SetEntryFlags(m_handle, GetFlags() & ~flags);
+}
+
+inline void NetworkTableEntry::SetPersistent() { SetFlags(kPersistent); }
+
+inline void NetworkTableEntry::ClearPersistent() { ClearFlags(kPersistent); }
+
+inline bool NetworkTableEntry::IsPersistent() const {
+  return (GetFlags() & kPersistent) != 0;
+}
+
+inline void NetworkTableEntry::Delete() { DeleteEntry(m_handle); }
+
+inline void NetworkTableEntry::CreateRpc(
+    std::function<void(const RpcAnswer& answer)> callback) {
+  ::nt::CreateRpc(m_handle, StringRef("\0", 1), callback);
+}
+
+inline RpcCall NetworkTableEntry::CallRpc(StringRef params) {
+  return RpcCall{m_handle, ::nt::CallRpc(m_handle, params)};
+}
+
+inline NT_EntryListener NetworkTableEntry::AddListener(
+    std::function<void(const EntryNotification& event)> callback,
+    unsigned int flags) const {
+  return AddEntryListener(m_handle, callback, flags);
+}
+
+inline void NetworkTableEntry::RemoveListener(NT_EntryListener entry_listener) {
+  RemoveEntryListener(entry_listener);
+}
+
+}  // namespace nt
+
+#endif  // NT_ENTRY_INL_
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableInstance.h b/ntcore/src/main/native/include/networktables/NetworkTableInstance.h
new file mode 100644
index 0000000..ee08745
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/NetworkTableInstance.h
@@ -0,0 +1,561 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NETWORKTABLES_NETWORKTABLEINSTANCE_H_
+#define NTCORE_NETWORKTABLES_NETWORKTABLEINSTANCE_H_
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <wpi/ArrayRef.h>
+#include <wpi/StringRef.h>
+#include <wpi/Twine.h>
+
+#include "networktables/NetworkTable.h"
+#include "networktables/NetworkTableEntry.h"
+#include "ntcore_c.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+using wpi::ArrayRef;
+using wpi::StringRef;
+using wpi::Twine;
+
+/**
+ * NetworkTables Instance.
+ *
+ * Instances are completely independent from each other.  Table operations on
+ * one instance will not be visible to other instances unless the instances are
+ * connected via the network.  The main limitation on instances is that you
+ * cannot have two servers on the same network port.  The main utility of
+ * instances is for unit testing, but they can also enable one program to
+ * connect to two different NetworkTables networks.
+ *
+ * The global "default" instance (as returned by GetDefault()) is
+ * always available, and is intended for the common case when there is only
+ * a single NetworkTables instance being used in the program.  The
+ * default instance cannot be destroyed.
+ *
+ * Additional instances can be created with the Create() function.
+ * Instances are not reference counted or RAII.  Instead, they must be
+ * explicitly destroyed (with Destroy()).
+ *
+ * @ingroup ntcore_cpp_api
+ */
+class NetworkTableInstance final {
+ public:
+  /**
+   * Client/server mode flag values (as returned by GetNetworkMode()).
+   * This is a bitmask.
+   */
+  enum NetworkMode {
+    kNetModeNone = NT_NET_MODE_NONE,
+    kNetModeServer = NT_NET_MODE_SERVER,
+    kNetModeClient = NT_NET_MODE_CLIENT,
+    kNetModeStarting = NT_NET_MODE_STARTING,
+    kNetModeFailure = NT_NET_MODE_FAILURE
+  };
+
+  /**
+   * Logging levels (as used by SetLogger()).
+   */
+  enum LogLevel {
+    kLogCritical = NT_LOG_CRITICAL,
+    kLogError = NT_LOG_ERROR,
+    kLogWarning = NT_LOG_WARNING,
+    kLogInfo = NT_LOG_INFO,
+    kLogDebug = NT_LOG_DEBUG,
+    kLogDebug1 = NT_LOG_DEBUG1,
+    kLogDebug2 = NT_LOG_DEBUG2,
+    kLogDebug3 = NT_LOG_DEBUG3,
+    kLogDebug4 = NT_LOG_DEBUG4
+  };
+
+  /**
+   * The default port that network tables operates on.
+   */
+  enum { kDefaultPort = NT_DEFAULT_PORT };
+
+  /**
+   * Construct invalid instance.
+   */
+  NetworkTableInstance() noexcept;
+
+  /**
+   * Construct from native handle.
+   *
+   * @param handle Native handle
+   */
+  explicit NetworkTableInstance(NT_Inst inst) noexcept;
+
+  /**
+   * Determines if the native handle is valid.
+   *
+   * @return True if the native handle is valid, false otherwise.
+   */
+  explicit operator bool() const { return m_handle != 0; }
+
+  /**
+   * Get global default instance.
+   *
+   * @return Global default instance
+   */
+  static NetworkTableInstance GetDefault();
+
+  /**
+   * Create an instance.
+   *
+   * @return Newly created instance
+   */
+  static NetworkTableInstance Create();
+
+  /**
+   * Destroys an instance (note: this has global effect).
+   *
+   * @param inst Instance
+   */
+  static void Destroy(NetworkTableInstance inst);
+
+  /**
+   * Gets the native handle for the entry.
+   *
+   * @return Native handle
+   */
+  NT_Inst GetHandle() const;
+
+  /**
+   * Gets the entry for a key.
+   *
+   * @param name Key
+   * @return Network table entry.
+   */
+  NetworkTableEntry GetEntry(const Twine& name);
+
+  /**
+   * Get entries starting with the given prefix.
+   *
+   * The results are optionally filtered by string prefix and entry type to
+   * only return a subset of all entries.
+   *
+   * @param prefix entry name required prefix; only entries whose name
+   * starts with this string are returned
+   * @param types bitmask of types; 0 is treated as a "don't care"
+   * @return Array of entries.
+   */
+  std::vector<NetworkTableEntry> GetEntries(const Twine& prefix,
+                                            unsigned int types);
+
+  /**
+   * Get information about entries starting with the given prefix.
+   *
+   * The results are optionally filtered by string prefix and entry type to
+   * only return a subset of all entries.
+   *
+   * @param prefix entry name required prefix; only entries whose name
+   * starts with this string are returned
+   * @param types bitmask of types; 0 is treated as a "don't care"
+   * @return Array of entry information.
+   */
+  std::vector<EntryInfo> GetEntryInfo(const Twine& prefix,
+                                      unsigned int types) const;
+
+  /**
+   * Gets the table with the specified key.
+   *
+   * @param key the key name
+   * @return The network table
+   */
+  std::shared_ptr<NetworkTable> GetTable(const Twine& key) const;
+
+  /**
+   * Deletes ALL keys in ALL subtables (except persistent values).
+   * Use with caution!
+   */
+  void DeleteAllEntries();
+
+  /**
+   * @{
+   * @name Entry Listener Functions
+   */
+
+  /**
+   * Add a listener for all entries starting with a certain prefix.
+   *
+   * @param prefix            UTF-8 string prefix
+   * @param callback          listener to add
+   * @param flags             EntryListenerFlags bitmask
+   * @return Listener handle
+   */
+  NT_EntryListener AddEntryListener(
+      const Twine& prefix,
+      std::function<void(const EntryNotification& event)> callback,
+      unsigned int flags) const;
+
+  /**
+   * Remove an entry listener.
+   *
+   * @param entry_listener Listener handle to remove
+   */
+  static void RemoveEntryListener(NT_EntryListener entry_listener);
+
+  /**
+   * Wait for the entry listener queue to be empty.  This is primarily useful
+   * for deterministic testing.  This blocks until either the entry listener
+   * queue is empty (e.g. there are no more events that need to be passed along
+   * to callbacks or poll queues) or the timeout expires.
+   *
+   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+   *                  or a negative value to block indefinitely
+   * @return False if timed out, otherwise true.
+   */
+  bool WaitForEntryListenerQueue(double timeout);
+
+  /** @} */
+
+  /**
+   * @{
+   * @name Connection Listener Functions
+   */
+
+  /**
+   * Add a connection listener.
+   *
+   * @param callback          listener to add
+   * @param immediate_notify  notify listener of all existing connections
+   * @return Listener handle
+   */
+  NT_ConnectionListener AddConnectionListener(
+      std::function<void(const ConnectionNotification& event)> callback,
+      bool immediate_notify) const;
+
+  /**
+   * Remove a connection listener.
+   *
+   * @param conn_listener Listener handle to remove
+   */
+  static void RemoveConnectionListener(NT_ConnectionListener conn_listener);
+
+  /**
+   * Wait for the connection listener queue to be empty.  This is primarily
+   * useful for deterministic testing.  This blocks until either the connection
+   * listener queue is empty (e.g. there are no more events that need to be
+   * passed along to callbacks or poll queues) or the timeout expires.
+   *
+   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+   *                  or a negative value to block indefinitely
+   * @return False if timed out, otherwise true.
+   */
+  bool WaitForConnectionListenerQueue(double timeout);
+
+  /** @} */
+
+  /**
+   * @{
+   * @name Remote Procedure Call Functions
+   */
+
+  /**
+   * Wait for the incoming RPC call queue to be empty.  This is primarily useful
+   * for deterministic testing.  This blocks until either the RPC call
+   * queue is empty (e.g. there are no more events that need to be passed along
+   * to callbacks or poll queues) or the timeout expires.
+   *
+   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+   *                  or a negative value to block indefinitely
+   * @return False if timed out, otherwise true.
+   */
+  bool WaitForRpcCallQueue(double timeout);
+
+  /** @} */
+
+  /**
+   * @{
+   * @name Client/Server Functions
+   */
+
+  /**
+   * Set the network identity of this node.
+   *
+   * This is the name used during the initial connection handshake, and is
+   * visible through ConnectionInfo on the remote node.
+   *
+   * @param name      identity to advertise
+   */
+  void SetNetworkIdentity(const Twine& name);
+
+  /**
+   * Get the current network mode.
+   *
+   * @return Bitmask of NetworkMode.
+   */
+  unsigned int GetNetworkMode() const;
+
+  /**
+   * Starts a server using the specified filename, listening address, and port.
+   *
+   * @param persist_filename  the name of the persist file to use (UTF-8 string,
+   *                          null terminated)
+   * @param listen_address    the address to listen on, or null to listen on any
+   *                          address (UTF-8 string, null terminated)
+   * @param port              port to communicate over
+   */
+  void StartServer(const Twine& persist_filename = "networktables.ini",
+                   const char* listen_address = "",
+                   unsigned int port = kDefaultPort);
+
+  /**
+   * Stops the server if it is running.
+   */
+  void StopServer();
+
+  /**
+   * Starts a client.  Use SetServer to set the server name and port.
+   */
+  void StartClient();
+
+  /**
+   * Starts a client using the specified server and port
+   *
+   * @param server_name server name (UTF-8 string, null terminated)
+   * @param port        port to communicate over
+   */
+  void StartClient(const char* server_name, unsigned int port = kDefaultPort);
+
+  /**
+   * Starts a client using the specified (server, port) combinations.  The
+   * client will attempt to connect to each server in round robin fashion.
+   *
+   * @param servers   array of server name and port pairs
+   */
+  void StartClient(ArrayRef<std::pair<StringRef, unsigned int>> servers);
+
+  /**
+   * Starts a client using the specified servers and port.  The
+   * client will attempt to connect to each server in round robin fashion.
+   *
+   * @param servers   array of server names
+   * @param port      port to communicate over
+   */
+  void StartClient(ArrayRef<StringRef> servers,
+                   unsigned int port = kDefaultPort);
+
+  /**
+   * Starts a client using commonly known robot addresses for the specified
+   * team.
+   *
+   * @param team        team number
+   * @param port        port to communicate over
+   */
+  void StartClientTeam(unsigned int team, unsigned int port = kDefaultPort);
+
+  /**
+   * Stops the client if it is running.
+   */
+  void StopClient();
+
+  /**
+   * Sets server address and port for client (without restarting client).
+   *
+   * @param server_name server name (UTF-8 string, null terminated)
+   * @param port        port to communicate over
+   */
+  void SetServer(const char* server_name, unsigned int port = kDefaultPort);
+
+  /**
+   * Sets server addresses and ports for client (without restarting client).
+   * The client will attempt to connect to each server in round robin fashion.
+   *
+   * @param servers   array of server name and port pairs
+   */
+  void SetServer(ArrayRef<std::pair<StringRef, unsigned int>> servers);
+
+  /**
+   * Sets server addresses and port for client (without restarting client).
+   * The client will attempt to connect to each server in round robin fashion.
+   *
+   * @param servers   array of server names
+   * @param port      port to communicate over
+   */
+  void SetServer(ArrayRef<StringRef> servers, unsigned int port = kDefaultPort);
+
+  /**
+   * Sets server addresses and port for client (without restarting client).
+   * Connects using commonly known robot addresses for the specified team.
+   *
+   * @param team        team number
+   * @param port        port to communicate over
+   */
+  void SetServerTeam(unsigned int team, unsigned int port = kDefaultPort);
+
+  /**
+   * Starts requesting server address from Driver Station.
+   * This connects to the Driver Station running on localhost to obtain the
+   * server IP address.
+   *
+   * @param port server port to use in combination with IP from DS
+   */
+  void StartDSClient(unsigned int port = kDefaultPort);
+
+  /**
+   * Stops requesting server address from Driver Station.
+   */
+  void StopDSClient();
+
+  /**
+   * Set the periodic update rate.
+   * Sets how frequently updates are sent to other nodes over the network.
+   *
+   * @param interval update interval in seconds (range 0.01 to 1.0)
+   */
+  void SetUpdateRate(double interval);
+
+  /**
+   * Flushes all updated values immediately to the network.
+   * @note This is rate-limited to protect the network from flooding.
+   * This is primarily useful for synchronizing network updates with
+   * user code.
+   */
+  void Flush() const;
+
+  /**
+   * Get information on the currently established network connections.
+   * If operating as a client, this will return either zero or one values.
+   *
+   * @return array of connection information
+   */
+  std::vector<ConnectionInfo> GetConnections() const;
+
+  /**
+   * Return whether or not the instance is connected to another node.
+   *
+   * @return True if connected.
+   */
+  bool IsConnected() const;
+
+  /** @} */
+
+  /**
+   * @{
+   * @name File Save/Load Functions
+   */
+
+  /**
+   * Save persistent values to a file.  The server automatically does this,
+   * but this function provides a way to save persistent values in the same
+   * format to a file on either a client or a server.
+   *
+   * @param filename  filename
+   * @return error string, or nullptr if successful
+   */
+  const char* SavePersistent(const Twine& filename) const;
+
+  /**
+   * Load persistent values from a file.  The server automatically does this
+   * at startup, but this function provides a way to restore persistent values
+   * in the same format from a file at any time on either a client or a server.
+   *
+   * @param filename  filename
+   * @param warn      callback function for warnings
+   * @return error string, or nullptr if successful
+   */
+  const char* LoadPersistent(
+      const Twine& filename,
+      std::function<void(size_t line, const char* msg)> warn);
+
+  /**
+   * Save table values to a file.  The file format used is identical to
+   * that used for SavePersistent.
+   *
+   * @param filename  filename
+   * @param prefix    save only keys starting with this prefix
+   * @return error string, or nullptr if successful
+   */
+  const char* SaveEntries(const Twine& filename, const Twine& prefix) const;
+
+  /**
+   * Load table values from a file.  The file format used is identical to
+   * that used for SavePersistent / LoadPersistent.
+   *
+   * @param filename  filename
+   * @param prefix    load only keys starting with this prefix
+   * @param warn      callback function for warnings
+   * @return error string, or nullptr if successful
+   */
+  const char* LoadEntries(
+      const Twine& filename, const Twine& prefix,
+      std::function<void(size_t line, const char* msg)> warn);
+
+  /** @} */
+
+  /**
+   * @{
+   * @name Logger Functions
+   */
+
+  /**
+   * Add logger callback function.  By default, log messages are sent to stderr;
+   * this function sends log messages with the specified levels to the provided
+   * callback function instead.  The callback function will only be called for
+   * log messages with level greater than or equal to minLevel and less than or
+   * equal to maxLevel; messages outside this range will be silently ignored.
+   *
+   * @param func        log callback function
+   * @param minLevel    minimum log level
+   * @param maxLevel    maximum log level
+   * @return Logger handle
+   */
+  NT_Logger AddLogger(std::function<void(const LogMessage& msg)> func,
+                      unsigned int min_level, unsigned int max_level);
+
+  /**
+   * Remove a logger.
+   *
+   * @param logger Logger handle to remove
+   */
+  static void RemoveLogger(NT_Logger logger);
+
+  /**
+   * Wait for the incoming log event queue to be empty.  This is primarily
+   * useful for deterministic testing.  This blocks until either the log event
+   * queue is empty (e.g. there are no more events that need to be passed along
+   * to callbacks or poll queues) or the timeout expires.
+   *
+   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+   *                  or a negative value to block indefinitely
+   * @return False if timed out, otherwise true.
+   */
+  bool WaitForLoggerQueue(double timeout);
+
+  /** @} */
+
+  /**
+   * Equality operator.  Returns true if both instances refer to the same
+   * native handle.
+   */
+  bool operator==(const NetworkTableInstance& other) const {
+    return m_handle == other.m_handle;
+  }
+
+  /** Inequality operator. */
+  bool operator!=(const NetworkTableInstance& other) const {
+    return !(*this == other);
+  }
+
+ private:
+  /* Native handle */
+  NT_Inst m_handle;
+};
+
+}  // namespace nt
+
+#include "networktables/NetworkTableInstance.inl"
+
+#endif  // NTCORE_NETWORKTABLES_NETWORKTABLEINSTANCE_H_
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableInstance.inl b/ntcore/src/main/native/include/networktables/NetworkTableInstance.inl
new file mode 100644
index 0000000..83616d9
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/NetworkTableInstance.inl
@@ -0,0 +1,187 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) FIRST 2017. All Rights Reserved.                             */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NT_INSTANCE_INL_
+#define NT_INSTANCE_INL_
+
+namespace nt {
+
+inline NetworkTableInstance::NetworkTableInstance() noexcept : m_handle{0} {}
+
+inline NetworkTableInstance::NetworkTableInstance(NT_Inst handle) noexcept
+    : m_handle{handle} {}
+
+inline NetworkTableInstance NetworkTableInstance::GetDefault() {
+  return NetworkTableInstance{GetDefaultInstance()};
+}
+
+inline NetworkTableInstance NetworkTableInstance::Create() {
+  return NetworkTableInstance{CreateInstance()};
+}
+
+inline void NetworkTableInstance::Destroy(NetworkTableInstance inst) {
+  if (inst.m_handle != 0) DestroyInstance(inst.m_handle);
+}
+
+inline NT_Inst NetworkTableInstance::GetHandle() const { return m_handle; }
+
+inline NetworkTableEntry NetworkTableInstance::GetEntry(const Twine& name) {
+  return NetworkTableEntry{::nt::GetEntry(m_handle, name)};
+}
+
+inline std::vector<NetworkTableEntry> NetworkTableInstance::GetEntries(
+    const Twine& prefix, unsigned int types) {
+  std::vector<NetworkTableEntry> entries;
+  for (auto entry : ::nt::GetEntries(m_handle, prefix, types))
+    entries.emplace_back(entry);
+  return entries;
+}
+
+inline std::vector<EntryInfo> NetworkTableInstance::GetEntryInfo(
+    const Twine& prefix, unsigned int types) const {
+  return ::nt::GetEntryInfo(m_handle, prefix, types);
+}
+
+inline void NetworkTableInstance::DeleteAllEntries() {
+  ::nt::DeleteAllEntries(m_handle);
+}
+
+inline void NetworkTableInstance::RemoveEntryListener(
+    NT_EntryListener entry_listener) {
+  ::nt::RemoveEntryListener(entry_listener);
+}
+
+inline bool NetworkTableInstance::WaitForEntryListenerQueue(double timeout) {
+  return ::nt::WaitForEntryListenerQueue(m_handle, timeout);
+}
+
+inline void NetworkTableInstance::RemoveConnectionListener(
+    NT_ConnectionListener conn_listener) {
+  ::nt::RemoveConnectionListener(conn_listener);
+}
+
+inline bool NetworkTableInstance::WaitForConnectionListenerQueue(
+    double timeout) {
+  return ::nt::WaitForConnectionListenerQueue(m_handle, timeout);
+}
+
+inline bool NetworkTableInstance::WaitForRpcCallQueue(double timeout) {
+  return ::nt::WaitForRpcCallQueue(m_handle, timeout);
+}
+
+inline void NetworkTableInstance::SetNetworkIdentity(const Twine& name) {
+  ::nt::SetNetworkIdentity(m_handle, name);
+}
+
+inline unsigned int NetworkTableInstance::GetNetworkMode() const {
+  return ::nt::GetNetworkMode(m_handle);
+}
+
+inline void NetworkTableInstance::StartServer(const Twine& persist_filename,
+                                              const char* listen_address,
+                                              unsigned int port) {
+  ::nt::StartServer(m_handle, persist_filename, listen_address, port);
+}
+
+inline void NetworkTableInstance::StopServer() { ::nt::StopServer(m_handle); }
+
+inline void NetworkTableInstance::StartClient() { ::nt::StartClient(m_handle); }
+
+inline void NetworkTableInstance::StartClient(const char* server_name,
+                                              unsigned int port) {
+  ::nt::StartClient(m_handle, server_name, port);
+}
+
+inline void NetworkTableInstance::StartClient(
+    ArrayRef<std::pair<StringRef, unsigned int>> servers) {
+  ::nt::StartClient(m_handle, servers);
+}
+
+inline void NetworkTableInstance::StartClientTeam(unsigned int team,
+                                                  unsigned int port) {
+  ::nt::StartClientTeam(m_handle, team, port);
+}
+
+inline void NetworkTableInstance::StopClient() { ::nt::StopClient(m_handle); }
+
+inline void NetworkTableInstance::SetServer(const char* server_name,
+                                            unsigned int port) {
+  ::nt::SetServer(m_handle, server_name, port);
+}
+
+inline void NetworkTableInstance::SetServer(
+    ArrayRef<std::pair<StringRef, unsigned int>> servers) {
+  ::nt::SetServer(m_handle, servers);
+}
+
+inline void NetworkTableInstance::SetServerTeam(unsigned int team,
+                                                unsigned int port) {
+  ::nt::SetServerTeam(m_handle, team, port);
+}
+
+inline void NetworkTableInstance::StartDSClient(unsigned int port) {
+  ::nt::StartDSClient(m_handle, port);
+}
+
+inline void NetworkTableInstance::StopDSClient() {
+  ::nt::StopDSClient(m_handle);
+}
+
+inline void NetworkTableInstance::SetUpdateRate(double interval) {
+  ::nt::SetUpdateRate(m_handle, interval);
+}
+
+inline void NetworkTableInstance::Flush() const { ::nt::Flush(m_handle); }
+
+inline std::vector<ConnectionInfo> NetworkTableInstance::GetConnections()
+    const {
+  return ::nt::GetConnections(m_handle);
+}
+
+inline bool NetworkTableInstance::IsConnected() const {
+  return ::nt::IsConnected(m_handle);
+}
+
+inline const char* NetworkTableInstance::SavePersistent(
+    const Twine& filename) const {
+  return ::nt::SavePersistent(m_handle, filename);
+}
+
+inline const char* NetworkTableInstance::LoadPersistent(
+    const Twine& filename,
+    std::function<void(size_t line, const char* msg)> warn) {
+  return ::nt::LoadPersistent(m_handle, filename, warn);
+}
+
+inline const char* NetworkTableInstance::SaveEntries(
+    const Twine& filename, const Twine& prefix) const {
+  return ::nt::SaveEntries(m_handle, filename, prefix);
+}
+
+inline const char* NetworkTableInstance::LoadEntries(
+    const Twine& filename, const Twine& prefix,
+    std::function<void(size_t line, const char* msg)> warn) {
+  return ::nt::LoadEntries(m_handle, filename, prefix, warn);
+}
+
+inline NT_Logger NetworkTableInstance::AddLogger(
+    std::function<void(const LogMessage& msg)> func, unsigned int min_level,
+    unsigned int max_level) {
+  return ::nt::AddLogger(m_handle, func, min_level, max_level);
+}
+
+inline void NetworkTableInstance::RemoveLogger(NT_Logger logger) {
+  ::nt::RemoveLogger(logger);
+}
+
+inline bool NetworkTableInstance::WaitForLoggerQueue(double timeout) {
+  return ::nt::WaitForLoggerQueue(m_handle, timeout);
+}
+
+}  // namespace nt
+
+#endif  // NT_INSTANCE_INL_
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableType.h b/ntcore/src/main/native/include/networktables/NetworkTableType.h
new file mode 100644
index 0000000..7ac3d9a
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/NetworkTableType.h
@@ -0,0 +1,33 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NETWORKTABLES_NETWORKTABLETYPE_H_
+#define NTCORE_NETWORKTABLES_NETWORKTABLETYPE_H_
+
+#include "ntcore_c.h"
+
+namespace nt {
+
+/**
+ * NetworkTable entry type.
+ * @ingroup ntcore_cpp_api
+ */
+enum class NetworkTableType {
+  kUnassigned = NT_UNASSIGNED,
+  kBoolean = NT_BOOLEAN,
+  kDouble = NT_DOUBLE,
+  kString = NT_STRING,
+  kRaw = NT_RAW,
+  kBooleanArray = NT_BOOLEAN_ARRAY,
+  kDoubleArray = NT_DOUBLE_ARRAY,
+  kStringArray = NT_STRING_ARRAY,
+  kRpc = NT_RPC
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_NETWORKTABLES_NETWORKTABLETYPE_H_
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableValue.h b/ntcore/src/main/native/include/networktables/NetworkTableValue.h
new file mode 100644
index 0000000..3aa0c01
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/NetworkTableValue.h
@@ -0,0 +1,458 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NETWORKTABLES_NETWORKTABLEVALUE_H_
+#define NTCORE_NETWORKTABLES_NETWORKTABLEVALUE_H_
+
+#include <stdint.h>
+
+#include <cassert>
+#include <memory>
+#include <string>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#include <wpi/ArrayRef.h>
+#include <wpi/StringRef.h>
+#include <wpi/Twine.h>
+
+#include "ntcore_c.h"
+
+namespace nt {
+
+using wpi::ArrayRef;
+using wpi::StringRef;
+using wpi::Twine;
+
+/**
+ * A network table entry value.
+ * @ingroup ntcore_cpp_api
+ */
+class Value final {
+  struct private_init {};
+
+ public:
+  Value();
+  Value(NT_Type type, uint64_t time, const private_init&);
+  ~Value();
+
+  /**
+   * Get the data type.
+   *
+   * @return The type.
+   */
+  NT_Type type() const { return m_val.type; }
+
+  /**
+   * Get the data value stored.
+   *
+   * @return The type.
+   */
+  const NT_Value& value() const { return m_val; }
+
+  /**
+   * Get the creation time of the value.
+   *
+   * @return The time, in the units returned by nt::Now().
+   */
+  uint64_t last_change() const { return m_val.last_change; }
+
+  /**
+   * Get the creation time of the value.
+   *
+   * @return The time, in the units returned by nt::Now().
+   */
+  uint64_t time() const { return m_val.last_change; }
+
+  /**
+   * @{
+   * @name Type Checkers
+   */
+
+  /**
+   * Determine if entry value contains a value or is unassigned.
+   *
+   * @return True if the entry value contains a value.
+   */
+  bool IsValid() const { return m_val.type != NT_UNASSIGNED; }
+
+  /**
+   * Determine if entry value contains a boolean.
+   *
+   * @return True if the entry value is of boolean type.
+   */
+  bool IsBoolean() const { return m_val.type == NT_BOOLEAN; }
+
+  /**
+   * Determine if entry value contains a double.
+   *
+   * @return True if the entry value is of double type.
+   */
+  bool IsDouble() const { return m_val.type == NT_DOUBLE; }
+
+  /**
+   * Determine if entry value contains a string.
+   *
+   * @return True if the entry value is of string type.
+   */
+  bool IsString() const { return m_val.type == NT_STRING; }
+
+  /**
+   * Determine if entry value contains a raw.
+   *
+   * @return True if the entry value is of raw type.
+   */
+  bool IsRaw() const { return m_val.type == NT_RAW; }
+
+  /**
+   * Determine if entry value contains a rpc definition.
+   *
+   * @return True if the entry value is of rpc definition type.
+   */
+  bool IsRpc() const { return m_val.type == NT_RPC; }
+
+  /**
+   * Determine if entry value contains a boolean array.
+   *
+   * @return True if the entry value is of boolean array type.
+   */
+  bool IsBooleanArray() const { return m_val.type == NT_BOOLEAN_ARRAY; }
+
+  /**
+   * Determine if entry value contains a double array.
+   *
+   * @return True if the entry value is of double array type.
+   */
+  bool IsDoubleArray() const { return m_val.type == NT_DOUBLE_ARRAY; }
+
+  /**
+   * Determine if entry value contains a string array.
+   *
+   * @return True if the entry value is of string array type.
+   */
+  bool IsStringArray() const { return m_val.type == NT_STRING_ARRAY; }
+
+  /** @} */
+
+  /**
+   * @{
+   * @name Type-Safe Getters
+   */
+
+  /**
+   * Get the entry's boolean value.
+   *
+   * @return The boolean value.
+   */
+  bool GetBoolean() const {
+    assert(m_val.type == NT_BOOLEAN);
+    return m_val.data.v_boolean != 0;
+  }
+
+  /**
+   * Get the entry's double value.
+   *
+   * @return The double value.
+   */
+  double GetDouble() const {
+    assert(m_val.type == NT_DOUBLE);
+    return m_val.data.v_double;
+  }
+
+  /**
+   * Get the entry's string value.
+   *
+   * @return The string value.
+   */
+  StringRef GetString() const {
+    assert(m_val.type == NT_STRING);
+    return m_string;
+  }
+
+  /**
+   * Get the entry's raw value.
+   *
+   * @return The raw value.
+   */
+  StringRef GetRaw() const {
+    assert(m_val.type == NT_RAW);
+    return m_string;
+  }
+
+  /**
+   * Get the entry's rpc definition value.
+   *
+   * @return The rpc definition value.
+   */
+  StringRef GetRpc() const {
+    assert(m_val.type == NT_RPC);
+    return m_string;
+  }
+
+  /**
+   * Get the entry's boolean array value.
+   *
+   * @return The boolean array value.
+   */
+  ArrayRef<int> GetBooleanArray() const {
+    assert(m_val.type == NT_BOOLEAN_ARRAY);
+    return ArrayRef<int>(m_val.data.arr_boolean.arr,
+                         m_val.data.arr_boolean.size);
+  }
+
+  /**
+   * Get the entry's double array value.
+   *
+   * @return The double array value.
+   */
+  ArrayRef<double> GetDoubleArray() const {
+    assert(m_val.type == NT_DOUBLE_ARRAY);
+    return ArrayRef<double>(m_val.data.arr_double.arr,
+                            m_val.data.arr_double.size);
+  }
+
+  /**
+   * Get the entry's string array value.
+   *
+   * @return The string array value.
+   */
+  ArrayRef<std::string> GetStringArray() const {
+    assert(m_val.type == NT_STRING_ARRAY);
+    return m_string_array;
+  }
+
+  /** @} */
+
+  /**
+   * @{
+   * @name Factory functions
+   */
+
+  /**
+   * Creates a boolean entry value.
+   *
+   * @param value the value
+   * @param time if nonzero, the creation time to use (instead of the current
+   *             time)
+   * @return The entry value
+   */
+  static std::shared_ptr<Value> MakeBoolean(bool value, uint64_t time = 0) {
+    auto val = std::make_shared<Value>(NT_BOOLEAN, time, private_init());
+    val->m_val.data.v_boolean = value;
+    return val;
+  }
+
+  /**
+   * Creates a double entry value.
+   *
+   * @param value the value
+   * @param time if nonzero, the creation time to use (instead of the current
+   *             time)
+   * @return The entry value
+   */
+  static std::shared_ptr<Value> MakeDouble(double value, uint64_t time = 0) {
+    auto val = std::make_shared<Value>(NT_DOUBLE, time, private_init());
+    val->m_val.data.v_double = value;
+    return val;
+  }
+
+  /**
+   * Creates a string entry value.
+   *
+   * @param value the value
+   * @param time if nonzero, the creation time to use (instead of the current
+   *             time)
+   * @return The entry value
+   */
+  static std::shared_ptr<Value> MakeString(const Twine& value,
+                                           uint64_t time = 0) {
+    auto val = std::make_shared<Value>(NT_STRING, time, private_init());
+    val->m_string = value.str();
+    val->m_val.data.v_string.str = const_cast<char*>(val->m_string.c_str());
+    val->m_val.data.v_string.len = val->m_string.size();
+    return val;
+  }
+
+/**
+ * Creates a string entry value.
+ *
+ * @param value the value
+ * @param time if nonzero, the creation time to use (instead of the current
+ *             time)
+ * @return The entry value
+ */
+#ifdef _MSC_VER
+  template <typename T,
+            typename = std::enable_if_t<std::is_same<T, std::string>>>
+#else
+  template <typename T,
+            typename std::enable_if<std::is_same<T, std::string>::value>::type>
+#endif
+  static std::shared_ptr<Value> MakeString(T&& value, uint64_t time = 0) {
+    auto val = std::make_shared<Value>(NT_STRING, time, private_init());
+    val->m_string = std::move(value);
+    val->m_val.data.v_string.str = const_cast<char*>(val->m_string.c_str());
+    val->m_val.data.v_string.len = val->m_string.size();
+    return val;
+  }
+
+  /**
+   * Creates a raw entry value.
+   *
+   * @param value the value
+   * @param time if nonzero, the creation time to use (instead of the current
+   *             time)
+   * @return The entry value
+   */
+  static std::shared_ptr<Value> MakeRaw(StringRef value, uint64_t time = 0) {
+    auto val = std::make_shared<Value>(NT_RAW, time, private_init());
+    val->m_string = value;
+    val->m_val.data.v_raw.str = const_cast<char*>(val->m_string.c_str());
+    val->m_val.data.v_raw.len = val->m_string.size();
+    return val;
+  }
+
+/**
+ * Creates a raw entry value.
+ *
+ * @param value the value
+ * @param time if nonzero, the creation time to use (instead of the current
+ *             time)
+ * @return The entry value
+ */
+#ifdef _MSC_VER
+  template <typename T,
+            typename = std::enable_if_t<std::is_same<T, std::string>>>
+#else
+  template <typename T,
+            typename std::enable_if<std::is_same<T, std::string>::value>::type>
+#endif
+  static std::shared_ptr<Value> MakeRaw(T&& value, uint64_t time = 0) {
+    auto val = std::make_shared<Value>(NT_RAW, time, private_init());
+    val->m_string = std::move(value);
+    val->m_val.data.v_raw.str = const_cast<char*>(val->m_string.c_str());
+    val->m_val.data.v_raw.len = val->m_string.size();
+    return val;
+  }
+
+  /**
+   * Creates a rpc entry value.
+   *
+   * @param value the value
+   * @param time if nonzero, the creation time to use (instead of the current
+   *             time)
+   * @return The entry value
+   */
+  static std::shared_ptr<Value> MakeRpc(StringRef value, uint64_t time = 0) {
+    auto val = std::make_shared<Value>(NT_RPC, time, private_init());
+    val->m_string = value;
+    val->m_val.data.v_raw.str = const_cast<char*>(val->m_string.c_str());
+    val->m_val.data.v_raw.len = val->m_string.size();
+    return val;
+  }
+
+  /**
+   * Creates a rpc entry value.
+   *
+   * @param value the value
+   * @param time if nonzero, the creation time to use (instead of the current
+   *             time)
+   * @return The entry value
+   */
+  template <typename T>
+  static std::shared_ptr<Value> MakeRpc(T&& value, uint64_t time = 0) {
+    auto val = std::make_shared<Value>(NT_RPC, time, private_init());
+    val->m_string = std::move(value);
+    val->m_val.data.v_raw.str = const_cast<char*>(val->m_string.c_str());
+    val->m_val.data.v_raw.len = val->m_string.size();
+    return val;
+  }
+
+  /**
+   * Creates a boolean array entry value.
+   *
+   * @param value the value
+   * @param time if nonzero, the creation time to use (instead of the current
+   *             time)
+   * @return The entry value
+   */
+  static std::shared_ptr<Value> MakeBooleanArray(ArrayRef<bool> value,
+                                                 uint64_t time = 0);
+
+  /**
+   * Creates a boolean array entry value.
+   *
+   * @param value the value
+   * @param time if nonzero, the creation time to use (instead of the current
+   *             time)
+   * @return The entry value
+   */
+  static std::shared_ptr<Value> MakeBooleanArray(ArrayRef<int> value,
+                                                 uint64_t time = 0);
+
+  /**
+   * Creates a double array entry value.
+   *
+   * @param value the value
+   * @param time if nonzero, the creation time to use (instead of the current
+   *             time)
+   * @return The entry value
+   */
+  static std::shared_ptr<Value> MakeDoubleArray(ArrayRef<double> value,
+                                                uint64_t time = 0);
+
+  /**
+   * Creates a string array entry value.
+   *
+   * @param value the value
+   * @param time if nonzero, the creation time to use (instead of the current
+   *             time)
+   * @return The entry value
+   */
+  static std::shared_ptr<Value> MakeStringArray(ArrayRef<std::string> value,
+                                                uint64_t time = 0);
+
+  /**
+   * Creates a string array entry value.
+   *
+   * @param value the value
+   * @param time if nonzero, the creation time to use (instead of the current
+   *             time)
+   * @return The entry value
+   *
+   * @note This function moves the values out of the vector.
+   */
+  static std::shared_ptr<Value> MakeStringArray(
+      std::vector<std::string>&& value, uint64_t time = 0);
+
+  /** @} */
+
+  Value(const Value&) = delete;
+  Value& operator=(const Value&) = delete;
+  friend bool operator==(const Value& lhs, const Value& rhs);
+
+ private:
+  NT_Value m_val;
+  std::string m_string;
+  std::vector<std::string> m_string_array;
+};
+
+bool operator==(const Value& lhs, const Value& rhs);
+inline bool operator!=(const Value& lhs, const Value& rhs) {
+  return !(lhs == rhs);
+}
+
+/**
+ * NetworkTable Value alias for similarity with Java.
+ * @ingroup ntcore_cpp_api
+ */
+typedef Value NetworkTableValue;
+
+}  // namespace nt
+
+#endif  // NTCORE_NETWORKTABLES_NETWORKTABLEVALUE_H_
diff --git a/ntcore/src/main/native/include/networktables/RpcCall.h b/ntcore/src/main/native/include/networktables/RpcCall.h
new file mode 100644
index 0000000..7d83140
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/RpcCall.h
@@ -0,0 +1,109 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NETWORKTABLES_RPCCALL_H_
+#define NTCORE_NETWORKTABLES_RPCCALL_H_
+
+#include <string>
+#include <utility>
+
+#include "ntcore_c.h"
+
+namespace nt {
+
+class NetworkTableEntry;
+
+/**
+ * NetworkTables Remote Procedure Call
+ * @ingroup ntcore_cpp_api
+ */
+class RpcCall final {
+ public:
+  /**
+   * Construct invalid instance.
+   */
+  RpcCall() : m_entry(0), m_call(0) {}
+
+  /**
+   * Construct from native handles.
+   *
+   * @param entry Entry handle
+   * @param call  Call handle
+   */
+  RpcCall(NT_Entry entry, NT_RpcCall call) : m_entry(entry), m_call(call) {}
+
+  RpcCall(RpcCall&& other);
+  RpcCall(const RpcCall&) = delete;
+  RpcCall& operator=(const RpcCall&) = delete;
+
+  /**
+   * Destructor.  Cancels the result if no other action taken.
+   */
+  ~RpcCall();
+
+  /**
+   * Determines if the native handle is valid.
+   *
+   * @return True if the native handle is valid, false otherwise.
+   */
+  explicit operator bool() const { return m_call != 0; }
+
+  /**
+   * Get the RPC entry.
+   *
+   * @return NetworkTableEntry for the RPC.
+   */
+  NetworkTableEntry GetEntry() const;
+
+  /**
+   * Get the call native handle.
+   *
+   * @return Native handle.
+   */
+  NT_RpcCall GetCall() const { return m_call; }
+
+  /**
+   * Get the result (return value).  This function blocks until
+   * the result is received.
+   *
+   * @param result      received result (output)
+   * @return False on error, true otherwise.
+   */
+  bool GetResult(std::string* result);
+
+  /**
+   * Get the result (return value).  This function blocks until
+   * the result is received or it times out.
+   *
+   * @param result      received result (output)
+   * @param timeout     timeout, in seconds
+   * @param timed_out   true if the timeout period elapsed (output)
+   * @return False on error or timeout, true otherwise.
+   */
+  bool GetResult(std::string* result, double timeout, bool* timed_out);
+
+  /**
+   * Ignore the result.  This function is non-blocking.
+   */
+  void CancelResult();
+
+  friend void swap(RpcCall& first, RpcCall& second) {
+    using std::swap;
+    swap(first.m_entry, second.m_entry);
+    swap(first.m_call, second.m_call);
+  }
+
+ private:
+  NT_Entry m_entry;
+  NT_RpcCall m_call;
+};
+
+}  // namespace nt
+
+#include "networktables/RpcCall.inl"
+
+#endif  // NTCORE_NETWORKTABLES_RPCCALL_H_
diff --git a/ntcore/src/main/native/include/networktables/RpcCall.inl b/ntcore/src/main/native/include/networktables/RpcCall.inl
new file mode 100644
index 0000000..d7dacf5
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/RpcCall.inl
@@ -0,0 +1,48 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) FIRST 2017. All Rights Reserved.                             */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NT_RPCCALL_INL_
+#define NT_RPCCALL_INL_
+
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline RpcCall::RpcCall(RpcCall&& other) : RpcCall() {
+  swap(*this, other);
+}
+
+inline RpcCall::~RpcCall() {
+  // automatically cancel result if user didn't request it
+  if (m_call != 0) CancelResult();
+}
+
+inline bool RpcCall::GetResult(std::string* result) {
+  if (GetRpcResult(m_entry, m_call, result)) {
+    m_call = 0;
+    return true;
+  }
+  return false;
+}
+
+inline bool RpcCall::GetResult(std::string* result, double timeout,
+                               bool* timed_out) {
+  if (GetRpcResult(m_entry, m_call, result, timeout, timed_out)) {
+    m_call = 0;
+    return true;
+  }
+  return false;
+}
+
+inline void RpcCall::CancelResult() {
+  CancelRpcResult(m_entry, m_call);
+  m_call = 0;
+}
+
+}  // namespace nt
+
+#endif  // NT_RPCCALL_INL_
diff --git a/ntcore/src/main/native/include/networktables/TableEntryListener.h b/ntcore/src/main/native/include/networktables/TableEntryListener.h
new file mode 100644
index 0000000..c4552678
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/TableEntryListener.h
@@ -0,0 +1,45 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NETWORKTABLES_TABLEENTRYLISTENER_H_
+#define NTCORE_NETWORKTABLES_TABLEENTRYLISTENER_H_
+
+#include <functional>
+#include <memory>
+
+#include <wpi/StringRef.h>
+
+namespace nt {
+
+class NetworkTable;
+class NetworkTableEntry;
+class Value;
+
+using wpi::StringRef;
+
+/**
+ * A listener that listens to changes in values in a NetworkTable.
+ *
+ * Called when a key-value pair is changed in a NetworkTable.
+ *
+ * @param table the table the key-value pair exists in
+ * @param key the key associated with the value that changed
+ * @param entry the entry associated with the value that changed
+ * @param value the new value
+ * @param flags update flags; for example, EntryListenerFlags.kNew if the key
+ * did not previously exist
+ *
+ * @ingroup ntcore_cpp_api
+ */
+typedef std::function<void(NetworkTable* table, StringRef name,
+                           NetworkTableEntry entry,
+                           std::shared_ptr<Value> value, int flags)>
+    TableEntryListener;
+
+}  // namespace nt
+
+#endif  // NTCORE_NETWORKTABLES_TABLEENTRYLISTENER_H_
diff --git a/ntcore/src/main/native/include/networktables/TableListener.h b/ntcore/src/main/native/include/networktables/TableListener.h
new file mode 100644
index 0000000..9940bad
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/TableListener.h
@@ -0,0 +1,39 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NETWORKTABLES_TABLELISTENER_H_
+#define NTCORE_NETWORKTABLES_TABLELISTENER_H_
+
+#include <functional>
+#include <memory>
+
+#include <wpi/StringRef.h>
+
+namespace nt {
+
+class NetworkTable;
+
+using wpi::StringRef;
+
+/**
+ * A listener that listens to new sub-tables in a NetworkTable.
+ *
+ * Called when a new table is created.
+ *
+ * @param parent the parent of the table
+ * @param name the name of the new table
+ * @param table the new table
+ *
+ * @ingroup ntcore_cpp_api
+ */
+typedef std::function<void(NetworkTable* parent, StringRef name,
+                           std::shared_ptr<NetworkTable> table)>
+    TableListener;
+
+}  // namespace nt
+
+#endif  // NTCORE_NETWORKTABLES_TABLELISTENER_H_
diff --git a/ntcore/src/main/native/include/ntcore.h b/ntcore/src/main/native/include/ntcore.h
new file mode 100644
index 0000000..ff0511a
--- /dev/null
+++ b/ntcore/src/main/native/include/ntcore.h
@@ -0,0 +1,19 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NTCORE_H_
+#define NTCORE_NTCORE_H_
+
+/* C API */
+#include "ntcore_c.h"
+
+#ifdef __cplusplus
+/* C++ API */
+#include "ntcore_cpp.h"
+#endif /* __cplusplus */
+
+#endif  // NTCORE_NTCORE_H_
diff --git a/ntcore/src/main/native/include/ntcore_c.h b/ntcore/src/main/native/include/ntcore_c.h
new file mode 100644
index 0000000..60773ea
--- /dev/null
+++ b/ntcore/src/main/native/include/ntcore_c.h
@@ -0,0 +1,2071 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NTCORE_C_H_
+#define NTCORE_NTCORE_C_H_
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+#include <cstddef>
+#else
+#include <stddef.h>
+#endif
+
+#include <wpi/deprecated.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @defgroup ntcore_c_api ntcore C API
+ *
+ * Handle-based interface for C.
+ *
+ * @{
+ */
+
+/** Typedefs */
+typedef int NT_Bool;
+
+typedef unsigned int NT_Handle;
+typedef NT_Handle NT_ConnectionListener;
+typedef NT_Handle NT_ConnectionListenerPoller;
+typedef NT_Handle NT_Entry;
+typedef NT_Handle NT_EntryListener;
+typedef NT_Handle NT_EntryListenerPoller;
+typedef NT_Handle NT_Inst;
+typedef NT_Handle NT_Logger;
+typedef NT_Handle NT_LoggerPoller;
+typedef NT_Handle NT_RpcCall;
+typedef NT_Handle NT_RpcCallPoller;
+
+/** Default network tables port number */
+#define NT_DEFAULT_PORT 1735
+
+/** NetworkTables data types. */
+enum NT_Type {
+  NT_UNASSIGNED = 0,
+  NT_BOOLEAN = 0x01,
+  NT_DOUBLE = 0x02,
+  NT_STRING = 0x04,
+  NT_RAW = 0x08,
+  NT_BOOLEAN_ARRAY = 0x10,
+  NT_DOUBLE_ARRAY = 0x20,
+  NT_STRING_ARRAY = 0x40,
+  NT_RPC = 0x80
+};
+
+/** NetworkTables entry flags. */
+enum NT_EntryFlags { NT_PERSISTENT = 0x01 };
+
+/** NetworkTables logging levels. */
+enum NT_LogLevel {
+  NT_LOG_CRITICAL = 50,
+  NT_LOG_ERROR = 40,
+  NT_LOG_WARNING = 30,
+  NT_LOG_INFO = 20,
+  NT_LOG_DEBUG = 10,
+  NT_LOG_DEBUG1 = 9,
+  NT_LOG_DEBUG2 = 8,
+  NT_LOG_DEBUG3 = 7,
+  NT_LOG_DEBUG4 = 6
+};
+
+/** NetworkTables notifier kinds. */
+enum NT_NotifyKind {
+  NT_NOTIFY_NONE = 0,
+  NT_NOTIFY_IMMEDIATE = 0x01, /* initial listener addition */
+  NT_NOTIFY_LOCAL = 0x02,     /* changed locally */
+  NT_NOTIFY_NEW = 0x04,       /* newly created entry */
+  NT_NOTIFY_DELETE = 0x08,    /* deleted */
+  NT_NOTIFY_UPDATE = 0x10,    /* value changed */
+  NT_NOTIFY_FLAGS = 0x20      /* flags changed */
+};
+
+/** Client/server modes */
+enum NT_NetworkMode {
+  NT_NET_MODE_NONE = 0x00,     /* not running */
+  NT_NET_MODE_SERVER = 0x01,   /* running in server mode */
+  NT_NET_MODE_CLIENT = 0x02,   /* running in client mode */
+  NT_NET_MODE_STARTING = 0x04, /* flag for starting (either client or server) */
+  NT_NET_MODE_FAILURE = 0x08,  /* flag for failure (either client or server) */
+};
+
+/*
+ * Structures
+ */
+
+/** A NetworkTables string. */
+struct NT_String {
+  /**
+   * String contents (UTF-8).
+   * The string is NOT required to be zero-terminated.
+   * When returned by the library, this is zero-terminated and allocated with
+   * std::malloc().
+   */
+  char* str;
+
+  /**
+   * Length of the string in bytes.  If the string happens to be zero
+   * terminated, this does not include the zero-termination.
+   */
+  size_t len;
+};
+
+/** NetworkTables Entry Value.  Note this is a typed union. */
+struct NT_Value {
+  enum NT_Type type;
+  uint64_t last_change;
+  union {
+    NT_Bool v_boolean;
+    double v_double;
+    struct NT_String v_string;
+    struct NT_String v_raw;
+    struct {
+      NT_Bool* arr;
+      size_t size;
+    } arr_boolean;
+    struct {
+      double* arr;
+      size_t size;
+    } arr_double;
+    struct {
+      struct NT_String* arr;
+      size_t size;
+    } arr_string;
+  } data;
+};
+
+/** NetworkTables Entry Information */
+struct NT_EntryInfo {
+  /** Entry handle */
+  NT_Entry entry;
+
+  /** Entry name */
+  struct NT_String name;
+
+  /** Entry type */
+  enum NT_Type type;
+
+  /** Entry flags */
+  unsigned int flags;
+
+  /** Timestamp of last change to entry (type or value). */
+  uint64_t last_change;
+};
+
+/** NetworkTables Connection Information */
+struct NT_ConnectionInfo {
+  /**
+   * The remote identifier (as set on the remote node by
+   * NetworkTableInstance::SetNetworkIdentity() or nt::SetNetworkIdentity()).
+   */
+  struct NT_String remote_id;
+
+  /** The IP address of the remote node. */
+  struct NT_String remote_ip;
+
+  /** The port number of the remote node. */
+  unsigned int remote_port;
+
+  /**
+   * The last time any update was received from the remote node (same scale as
+   * returned by nt::Now()).
+   */
+  uint64_t last_update;
+
+  /**
+   * The protocol version being used for this connection.  This in protocol
+   * layer format, so 0x0200 = 2.0, 0x0300 = 3.0).
+   */
+  unsigned int protocol_version;
+};
+
+/** NetworkTables RPC Version 1 Definition Parameter */
+struct NT_RpcParamDef {
+  struct NT_String name;
+  struct NT_Value def_value;
+};
+
+/** NetworkTables RPC Version 1 Definition Result */
+struct NT_RpcResultDef {
+  struct NT_String name;
+  enum NT_Type type;
+};
+
+/** NetworkTables RPC Version 1 Definition */
+struct NT_RpcDefinition {
+  unsigned int version;
+  struct NT_String name;
+  size_t num_params;
+  struct NT_RpcParamDef* params;
+  size_t num_results;
+  struct NT_RpcResultDef* results;
+};
+
+/** NetworkTables RPC Call Data */
+struct NT_RpcAnswer {
+  NT_Entry entry;
+  NT_RpcCall call;
+  struct NT_String name;
+  struct NT_String params;
+  struct NT_ConnectionInfo conn;
+};
+
+/** NetworkTables Entry Notification */
+struct NT_EntryNotification {
+  /** Listener that was triggered. */
+  NT_EntryListener listener;
+
+  /** Entry handle. */
+  NT_Entry entry;
+
+  /** Entry name. */
+  struct NT_String name;
+
+  /** The new value. */
+  struct NT_Value value;
+
+  /**
+   * Update flags.  For example, NT_NOTIFY_NEW if the key did not previously
+   * exist.
+   */
+  unsigned int flags;
+};
+
+/** NetworkTables Connection Notification */
+struct NT_ConnectionNotification {
+  /** Listener that was triggered. */
+  NT_ConnectionListener listener;
+
+  /** True if event is due to connection being established. */
+  NT_Bool connected;
+
+  /** Connection info. */
+  struct NT_ConnectionInfo conn;
+};
+
+/** NetworkTables log message. */
+struct NT_LogMessage {
+  /** The logger that generated the message. */
+  NT_Logger logger;
+
+  /** Log level of the message.  See NT_LogLevel. */
+  unsigned int level;
+
+  /** The filename of the source file that generated the message. */
+  const char* filename;
+
+  /** The line number in the source file that generated the message. */
+  unsigned int line;
+
+  /** The message. */
+  char* message;
+};
+
+/**
+ * @defgroup ntcore_instance_cfunc Instance Functions
+ * @{
+ */
+
+/**
+ * Get default instance.
+ * This is the instance used by non-handle-taking functions.
+ *
+ * @return Instance handle
+ */
+NT_Inst NT_GetDefaultInstance(void);
+
+/**
+ * Create an instance.
+ *
+ * @return Instance handle
+ */
+NT_Inst NT_CreateInstance(void);
+
+/**
+ * Destroy an instance.
+ * The default instance cannot be destroyed.
+ *
+ * @param inst Instance handle
+ */
+void NT_DestroyInstance(NT_Inst inst);
+
+/**
+ * Get instance handle from another handle.
+ *
+ * @param handle    handle
+ * @return Instance handle
+ */
+NT_Inst NT_GetInstanceFromHandle(NT_Handle handle);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_table_cfunc Table Functions
+ * @{
+ */
+
+/**
+ * Get Entry Handle.
+ *
+ * @param inst      instance handle
+ * @param name      entry name (UTF-8 string)
+ * @param name_len  length of name in bytes
+ * @return entry handle
+ */
+NT_Entry NT_GetEntry(NT_Inst inst, const char* name, size_t name_len);
+
+/**
+ * Get Entry Handles.
+ *
+ * Returns an array of entry handles.  The results are optionally
+ * filtered by string prefix and entry type to only return a subset of all
+ * entries.
+ *
+ * @param prefix        entry name required prefix; only entries whose name
+ *                      starts with this string are returned
+ * @param prefix_len    length of prefix in bytes
+ * @param types         bitmask of NT_Type values; 0 is treated specially
+ *                      as a "don't care"
+ * @return Array of entry handles.
+ */
+NT_Entry* NT_GetEntries(NT_Inst inst, const char* prefix, size_t prefix_len,
+                        unsigned int types, size_t* count);
+
+/**
+ * Gets the name of the specified entry.
+ * Returns an empty string if the handle is invalid.
+ *
+ * @param entry     entry handle
+ * @param name_len  length of the returned string (output parameter)
+ * @return Entry name
+ */
+char* NT_GetEntryName(NT_Entry entry, size_t* name_len);
+
+/**
+ * Gets the type for the specified key, or unassigned if non existent.
+ *
+ * @param entry   entry handle
+ * @return Entry type
+ */
+enum NT_Type NT_GetEntryType(NT_Entry entry);
+
+/**
+ * Gets the last time the entry was changed.
+ * Returns 0 if the handle is invalid.
+ *
+ * @param entry   entry handle
+ * @return Entry last change time
+ */
+uint64_t NT_GetEntryLastChange(NT_Entry entry);
+
+/**
+ * Get Entry Value.
+ *
+ * Returns copy of current entry value.
+ * Note that one of the type options is "unassigned".
+ *
+ * @param entry     entry handle
+ * @param value     storage for returned entry value
+ *
+ * It is the caller's responsibility to free value once it's no longer
+ * needed (the utility function NT_DisposeValue() is useful for this
+ * purpose).
+ */
+void NT_GetEntryValue(NT_Entry entry, struct NT_Value* value);
+
+/**
+ * Set Default Entry Value.
+ *
+ * Returns copy of current entry value if it exists.
+ * Otherwise, sets passed in value, and returns set value.
+ * Note that one of the type options is "unassigned".
+ *
+ * @param entry     entry handle
+ * @param default_value     value to be set if name does not exist
+ * @return 0 on error (value not set), 1 on success
+ */
+NT_Bool NT_SetDefaultEntryValue(NT_Entry entry,
+                                const struct NT_Value* default_value);
+
+/**
+ * Set Entry Value.
+ *
+ * Sets new entry value.  If type of new value differs from the type of the
+ * currently stored entry, returns error and does not update value.
+ *
+ * @param entry     entry handle
+ * @param value     new entry value
+ * @return 0 on error (type mismatch), 1 on success
+ */
+NT_Bool NT_SetEntryValue(NT_Entry entry, const struct NT_Value* value);
+
+/**
+ * Set Entry Type and Value.
+ *
+ * Sets new entry value.  If type of new value differs from the type of the
+ * currently stored entry, the currently stored entry type is overridden
+ * (generally this will generate an Entry Assignment message).
+ *
+ * This is NOT the preferred method to update a value; generally
+ * NT_SetEntryValue() should be used instead, with appropriate error handling.
+ *
+ * @param entry     entry handle
+ * @param value     new entry value
+ */
+void NT_SetEntryTypeValue(NT_Entry entry, const struct NT_Value* value);
+
+/**
+ * Set Entry Flags.
+ *
+ * @param entry     entry handle
+ * @param flags     flags value (bitmask of NT_EntryFlags)
+ */
+void NT_SetEntryFlags(NT_Entry entry, unsigned int flags);
+
+/**
+ * Get Entry Flags.
+ *
+ * @param entry     entry handle
+ * @return Flags value (bitmask of NT_EntryFlags)
+ */
+unsigned int NT_GetEntryFlags(NT_Entry entry);
+
+/**
+ * Delete Entry.
+ *
+ * Deletes an entry.  This is a new feature in version 3.0 of the protocol,
+ * so this may not have an effect if any other node in the network is not
+ * version 3.0 or newer.
+ *
+ * Note: NT_GetConnections() can be used to determine the protocol version
+ * of direct remote connection(s), but this is not sufficient to determine
+ * if all nodes in the network are version 3.0 or newer.
+ *
+ * @param entry     entry handle
+ */
+void NT_DeleteEntry(NT_Entry entry);
+
+/**
+ * Delete All Entries.
+ *
+ * Deletes ALL table entries.  This is a new feature in version 3.0 of the
+ * so this may not have an effect if any other node in the network is not
+ * version 3.0 or newer.
+ *
+ * Note: NT_GetConnections() can be used to determine the protocol version
+ * of direct remote connection(s), but this is not sufficient to determine
+ * if all nodes in the network are version 3.0 or newer.
+ *
+ * @param inst      instance handle
+ */
+void NT_DeleteAllEntries(NT_Inst inst);
+
+/**
+ * Get Entry Information.
+ *
+ * Returns an array of entry information (entry handle, name, entry type,
+ * and timestamp of last change to type/value).  The results are optionally
+ * filtered by string prefix and entry type to only return a subset of all
+ * entries.
+ *
+ * @param inst          instance handle
+ * @param prefix        entry name required prefix; only entries whose name
+ *                      starts with this string are returned
+ * @param prefix_len    length of prefix in bytes
+ * @param types         bitmask of NT_Type values; 0 is treated specially
+ *                      as a "don't care"
+ * @param count         output parameter; set to length of returned array
+ * @return Array of entry information.
+ */
+struct NT_EntryInfo* NT_GetEntryInfo(NT_Inst inst, const char* prefix,
+                                     size_t prefix_len, unsigned int types,
+                                     size_t* count);
+
+/**
+ * Get Entry Information.
+ *
+ * Returns information about an entry (name, entry type,
+ * and timestamp of last change to type/value).
+ *
+ * @param entry         entry handle
+ * @param info          entry information (output)
+ * @return True if successful, false on error.
+ */
+NT_Bool NT_GetEntryInfoHandle(NT_Entry entry, struct NT_EntryInfo* info);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_entrylistener_cfunc Entry Listener Functions
+ * @{
+ */
+
+/**
+ * Entry listener callback function.
+ * Called when a key-value pair is changed.
+ *
+ * @param data            data pointer provided to callback creation function
+ * @param event           event information
+ */
+typedef void (*NT_EntryListenerCallback)(
+    void* data, const struct NT_EntryNotification* event);
+
+/**
+ * Add a listener for all entries starting with a certain prefix.
+ *
+ * @param inst              instance handle
+ * @param prefix            UTF-8 string prefix
+ * @param prefix_len        length of prefix in bytes
+ * @param data              data pointer to pass to callback
+ * @param callback          listener to add
+ * @param flags             NT_NotifyKind bitmask
+ * @return Listener handle
+ */
+NT_EntryListener NT_AddEntryListener(NT_Inst inst, const char* prefix,
+                                     size_t prefix_len, void* data,
+                                     NT_EntryListenerCallback callback,
+                                     unsigned int flags);
+
+/**
+ * Add a listener for a single entry.
+ *
+ * @param entry             entry handle
+ * @param data              data pointer to pass to callback
+ * @param callback          listener to add
+ * @param flags             NT_NotifyKind bitmask
+ * @return Listener handle
+ */
+NT_EntryListener NT_AddEntryListenerSingle(NT_Entry entry, void* data,
+                                           NT_EntryListenerCallback callback,
+                                           unsigned int flags);
+
+/**
+ * Create a entry listener poller.
+ *
+ * A poller provides a single queue of poll events.  Events linked to this
+ * poller (using NT_AddPolledEntryListener()) will be stored in the queue and
+ * must be collected by calling NT_PollEntryListener().
+ * The returned handle must be destroyed with NT_DestroyEntryListenerPoller().
+ *
+ * @param inst      instance handle
+ * @return poller handle
+ */
+NT_EntryListenerPoller NT_CreateEntryListenerPoller(NT_Inst inst);
+
+/**
+ * Destroy a entry listener poller.  This will abort any blocked polling
+ * call and prevent additional events from being generated for this poller.
+ *
+ * @param poller    poller handle
+ */
+void NT_DestroyEntryListenerPoller(NT_EntryListenerPoller poller);
+
+/**
+ * Create a polled entry listener.
+ * The caller is responsible for calling NT_PollEntryListener() to poll.
+ *
+ * @param poller            poller handle
+ * @param prefix            UTF-8 string prefix
+ * @param flags             NT_NotifyKind bitmask
+ * @return Listener handle
+ */
+NT_EntryListener NT_AddPolledEntryListener(NT_EntryListenerPoller poller,
+                                           const char* prefix,
+                                           size_t prefix_len,
+                                           unsigned int flags);
+
+/**
+ * Create a polled entry listener.
+ * The caller is responsible for calling NT_PollEntryListener() to poll.
+ *
+ * @param poller            poller handle
+ * @param prefix            UTF-8 string prefix
+ * @param flags             NT_NotifyKind bitmask
+ * @return Listener handle
+ */
+NT_EntryListener NT_AddPolledEntryListenerSingle(NT_EntryListenerPoller poller,
+                                                 NT_Entry entry,
+                                                 unsigned int flags);
+
+/**
+ * Get the next entry listener event.  This blocks until the next event occurs.
+ *
+ * This is intended to be used with NT_AddPolledEntryListener(void); entry
+ * listeners created using NT_AddEntryListener() will not be serviced through
+ * this function.
+ *
+ * @param poller    poller handle
+ * @param len       length of returned array (output)
+ * @return Array of information on the entry listener events.  Returns NULL if
+ *         an erroroccurred (e.g. the instance was invalid or is shutting down).
+ */
+struct NT_EntryNotification* NT_PollEntryListener(NT_EntryListenerPoller poller,
+                                                  size_t* len);
+
+/**
+ * Get the next entry listener event.  This blocks until the next event occurs
+ * or it times out.  This is intended to be used with
+ * NT_AddPolledEntryListener(); entry listeners created using
+ * NT_AddEntryListener() will not be serviced through this function.
+ *
+ * @param poller      poller handle
+ * @param len         length of returned array (output)
+ * @param timeout     timeout, in seconds
+ * @param timed_out   true if the timeout period elapsed (output)
+ * @return Array of information on the entry listener events.  If NULL is
+ *         returned and timed_out is also false, an error occurred (e.g. the
+ *         instance was invalid or is shutting down).
+ */
+struct NT_EntryNotification* NT_PollEntryListenerTimeout(
+    NT_EntryListenerPoller poller, size_t* len, double timeout,
+    NT_Bool* timed_out);
+
+/**
+ * Cancel a PollEntryListener call.  This wakes up a call to
+ * PollEntryListener for this poller and causes it to immediately return
+ * an empty array.
+ *
+ * @param poller  poller handle
+ */
+void NT_CancelPollEntryListener(NT_EntryListenerPoller poller);
+
+/**
+ * Remove an entry listener.
+ *
+ * @param entry_listener Listener handle to remove
+ */
+void NT_RemoveEntryListener(NT_EntryListener entry_listener);
+
+/**
+ * Wait for the entry listener queue to be empty.  This is primarily useful
+ * for deterministic testing.  This blocks until either the entry listener
+ * queue is empty (e.g. there are no more events that need to be passed along
+ * to callbacks or poll queues) or the timeout expires.
+ *
+ * @param inst      instance handle
+ * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+ *                  or a negative value to block indefinitely
+ * @return False if timed out, otherwise true.
+ */
+NT_Bool NT_WaitForEntryListenerQueue(NT_Inst inst, double timeout);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_connectionlistener_cfunc Connection Listener Functions
+ * @{
+ */
+
+/**
+ * Connection listener callback function.
+ * Called when a network connection is made or lost.
+ *
+ * @param data            data pointer provided to callback creation function
+ * @param event           event info
+ */
+typedef void (*NT_ConnectionListenerCallback)(
+    void* data, const struct NT_ConnectionNotification* event);
+
+/**
+ * Add a connection listener.
+ *
+ * @param inst              instance handle
+ * @param data              data pointer to pass to callback
+ * @param callback          listener to add
+ * @param immediate_notify  notify listener of all existing connections
+ * @return Listener handle
+ */
+NT_ConnectionListener NT_AddConnectionListener(
+    NT_Inst inst, void* data, NT_ConnectionListenerCallback callback,
+    NT_Bool immediate_notify);
+
+/**
+ * Create a connection listener poller.
+ * A poller provides a single queue of poll events.  Events linked to this
+ * poller (using NT_AddPolledConnectionListener()) will be stored in the queue
+ * and must be collected by calling NT_PollConnectionListener().
+ * The returned handle must be destroyed with
+ * NT_DestroyConnectionListenerPoller().
+ *
+ * @param inst      instance handle
+ * @return poller handle
+ */
+NT_ConnectionListenerPoller NT_CreateConnectionListenerPoller(NT_Inst inst);
+
+/**
+ * Destroy a connection listener poller.  This will abort any blocked polling
+ * call and prevent additional events from being generated for this poller.
+ *
+ * @param poller    poller handle
+ */
+void NT_DestroyConnectionListenerPoller(NT_ConnectionListenerPoller poller);
+
+/**
+ * Create a polled connection listener.
+ * The caller is responsible for calling NT_PollConnectionListener() to poll.
+ *
+ * @param poller            poller handle
+ * @param immediate_notify  notify listener of all existing connections
+ */
+NT_ConnectionListener NT_AddPolledConnectionListener(
+    NT_ConnectionListenerPoller poller, NT_Bool immediate_notify);
+
+/**
+ * Get the next connection event.  This blocks until the next connect or
+ * disconnect occurs.  This is intended to be used with
+ * NT_AddPolledConnectionListener(); connection listeners created using
+ * NT_AddConnectionListener() will not be serviced through this function.
+ *
+ * @param poller    poller handle
+ * @param len       length of returned array (output)
+ * @return Array of information on the connection events.  Only returns NULL
+ *         if an error occurred (e.g. the instance was invalid or is shutting
+ *         down).
+ */
+struct NT_ConnectionNotification* NT_PollConnectionListener(
+    NT_ConnectionListenerPoller poller, size_t* len);
+
+/**
+ * Get the next connection event.  This blocks until the next connect or
+ * disconnect occurs or it times out.  This is intended to be used with
+ * NT_AddPolledConnectionListener(); connection listeners created using
+ * NT_AddConnectionListener() will not be serviced through this function.
+ *
+ * @param poller      poller handle
+ * @param len         length of returned array (output)
+ * @param timeout     timeout, in seconds
+ * @param timed_out   true if the timeout period elapsed (output)
+ * @return Array of information on the connection events.  If NULL is returned
+ *         and timed_out is also false, an error occurred (e.g. the instance
+ *         was invalid or is shutting down).
+ */
+struct NT_ConnectionNotification* NT_PollConnectionListenerTimeout(
+    NT_ConnectionListenerPoller poller, size_t* len, double timeout,
+    NT_Bool* timed_out);
+
+/**
+ * Cancel a PollConnectionListener call.  This wakes up a call to
+ * PollConnectionListener for this poller and causes it to immediately return
+ * an empty array.
+ *
+ * @param poller  poller handle
+ */
+void NT_CancelPollConnectionListener(NT_ConnectionListenerPoller poller);
+
+/**
+ * Remove a connection listener.
+ *
+ * @param conn_listener Listener handle to remove
+ */
+void NT_RemoveConnectionListener(NT_ConnectionListener conn_listener);
+
+/**
+ * Wait for the connection listener queue to be empty.  This is primarily useful
+ * for deterministic testing.  This blocks until either the connection listener
+ * queue is empty (e.g. there are no more events that need to be passed along
+ * to callbacks or poll queues) or the timeout expires.
+ *
+ * @param inst      instance handle
+ * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+ *                  or a negative value to block indefinitely
+ * @return False if timed out, otherwise true.
+ */
+NT_Bool NT_WaitForConnectionListenerQueue(NT_Inst inst, double timeout);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_rpc_cfunc Remote Procedure Call Functions
+ * @{
+ */
+
+/**
+ * Remote Procedure Call (RPC) callback function.
+ *
+ * @param data        data pointer provided to NT_CreateRpc()
+ * @param call        call information
+ *
+ * Note: NT_PostRpcResponse() must be called by the callback to provide a
+ * response to the call.
+ */
+typedef void (*NT_RpcCallback)(void* data, const struct NT_RpcAnswer* call);
+
+/**
+ * Create a callback-based RPC entry point.  Only valid to use on the server.
+ * The callback function will be called when the RPC is called.
+ *
+ * @param entry     entry handle of RPC entry
+ * @param def       RPC definition
+ * @param def_len   length of def in bytes
+ * @param data      data pointer to pass to callback function
+ * @param callback  callback function
+ */
+void NT_CreateRpc(NT_Entry entry, const char* def, size_t def_len, void* data,
+                  NT_RpcCallback callback);
+
+/**
+ * Create a RPC call poller.  Only valid to use on the server.
+ *
+ * A poller provides a single queue of poll events.  Events linked to this
+ * poller (using NT_CreatePolledRpc()) will be stored in the queue and must be
+ * collected by calling NT_PollRpc() or NT_PollRpcTimeout().
+ * The returned handle must be destroyed with NT_DestroyRpcCallPoller().
+ *
+ * @param inst      instance handle
+ * @return poller handle
+ */
+NT_RpcCallPoller NT_CreateRpcCallPoller(NT_Inst inst);
+
+/**
+ * Destroy a RPC call poller.  This will abort any blocked polling call and
+ * prevent additional events from being generated for this poller.
+ *
+ * @param poller    poller handle
+ */
+void NT_DestroyRpcCallPoller(NT_RpcCallPoller poller);
+
+/**
+ * Create a polled RPC entry point.  Only valid to use on the server.
+ *
+ * The caller is responsible for calling NT_PollRpc() or NT_PollRpcTimeout()
+ * to poll for servicing incoming RPC calls.
+ *
+ * @param entry     entry handle of RPC entry
+ * @param def       RPC definition
+ * @param def_len   length of def in bytes
+ * @param poller    poller handle
+ */
+void NT_CreatePolledRpc(NT_Entry entry, const char* def, size_t def_len,
+                        NT_RpcCallPoller poller);
+
+/**
+ * Get the next incoming RPC call.  This blocks until the next incoming RPC
+ * call is received.  This is intended to be used with NT_CreatePolledRpc(void);
+ * RPC calls created using NT_CreateRpc() will not be serviced through this
+ * function.  Upon successful return, NT_PostRpcResponse() must be called to
+ * send the return value to the caller.  The returned array must be freed
+ * using NT_DisposeRpcAnswerArray().
+ *
+ * @param poller      poller handle
+ * @param len         length of returned array (output)
+ * @return Array of RPC call information.  Only returns NULL if an error
+ *         occurred (e.g. the instance was invalid or is shutting down).
+ */
+struct NT_RpcAnswer* NT_PollRpc(NT_RpcCallPoller poller, size_t* len);
+
+/**
+ * Get the next incoming RPC call.  This blocks until the next incoming RPC
+ * call is received or it times out.  This is intended to be used with
+ * NT_CreatePolledRpc(); RPC calls created using NT_CreateRpc() will not be
+ * serviced through this function.  Upon successful return,
+ * NT_PostRpcResponse() must be called to send the return value to the caller.
+ * The returned array must be freed using NT_DisposeRpcAnswerArray().
+ *
+ * @param poller      poller handle
+ * @param len         length of returned array (output)
+ * @param timeout     timeout, in seconds
+ * @param timed_out   true if the timeout period elapsed (output)
+ * @return Array of RPC call information.  If NULL is returned and timed_out
+ *         is also false, an error occurred (e.g. the instance was invalid or
+ *         is shutting down).
+ */
+struct NT_RpcAnswer* NT_PollRpcTimeout(NT_RpcCallPoller poller, size_t* len,
+                                       double timeout, NT_Bool* timed_out);
+
+/**
+ * Cancel a PollRpc call.  This wakes up a call to PollRpc for this poller
+ * and causes it to immediately return an empty array.
+ *
+ * @param poller  poller handle
+ */
+void NT_CancelPollRpc(NT_RpcCallPoller poller);
+
+/**
+ * Wait for the incoming RPC call queue to be empty.  This is primarily useful
+ * for deterministic testing.  This blocks until either the RPC call
+ * queue is empty (e.g. there are no more events that need to be passed along
+ * to callbacks or poll queues) or the timeout expires.
+ *
+ * @param inst      instance handle
+ * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+ *                  or a negative value to block indefinitely
+ * @return False if timed out, otherwise true.
+ */
+NT_Bool NT_WaitForRpcCallQueue(NT_Inst inst, double timeout);
+
+/**
+ * Post RPC response (return value) for a polled RPC.
+ *
+ * The rpc and call parameters should come from the NT_RpcAnswer returned
+ * by NT_PollRpc().
+ *
+ * @param entry       entry handle of RPC entry (from NT_RpcAnswer)
+ * @param call        RPC call handle (from NT_RpcAnswer)
+ * @param result      result raw data that will be provided to remote caller
+ * @param result_len  length of result in bytes
+ * @return            true if the response was posted, otherwise false
+ */
+NT_Bool NT_PostRpcResponse(NT_Entry entry, NT_RpcCall call, const char* result,
+                           size_t result_len);
+
+/**
+ * Call a RPC function.  May be used on either the client or server.
+ *
+ * This function is non-blocking.  Either NT_GetRpcResult() or
+ * NT_CancelRpcResult() must be called to either get or ignore the result of
+ * the call.
+ *
+ * @param entry       entry handle of RPC entry
+ * @param params      parameter
+ * @param params_len  length of param in bytes
+ * @return RPC call handle (for use with NT_GetRpcResult() or
+ *         NT_CancelRpcResult()).
+ */
+NT_RpcCall NT_CallRpc(NT_Entry entry, const char* params, size_t params_len);
+
+/**
+ * Get the result (return value) of a RPC call.  This function blocks until
+ * the result is received.
+ *
+ * @param entry       entry handle of RPC entry
+ * @param call        RPC call handle returned by NT_CallRpc()
+ * @param result_len  length of returned result in bytes
+ * @return NULL on error, or result.
+ */
+char* NT_GetRpcResult(NT_Entry entry, NT_RpcCall call, size_t* result_len);
+
+/**
+ * Get the result (return value) of a RPC call.  This function blocks until
+ * the result is received or it times out.
+ *
+ * @param entry       entry handle of RPC entry
+ * @param call        RPC call handle returned by NT_CallRpc()
+ * @param result_len  length of returned result in bytes
+ * @param timeout     timeout, in seconds
+ * @param timed_out   true if the timeout period elapsed (output)
+ * @return NULL on error or timeout, or result.
+ */
+char* NT_GetRpcResultTimeout(NT_Entry entry, NT_RpcCall call,
+                             size_t* result_len, double timeout,
+                             NT_Bool* timed_out);
+
+/**
+ * Ignore the result of a RPC call.  This function is non-blocking.
+ *
+ * @param entry       entry handle of RPC entry
+ * @param call        RPC call handle returned by NT_CallRpc()
+ */
+void NT_CancelRpcResult(NT_Entry entry, NT_RpcCall call);
+
+/**
+ * Pack a RPC version 1 definition.
+ *
+ * @param def         RPC version 1 definition
+ * @param packed_len  length of return value in bytes
+ * @return Raw packed bytes.  Use C standard library std::free() to release.
+ */
+char* NT_PackRpcDefinition(const struct NT_RpcDefinition* def,
+                           size_t* packed_len);
+
+/**
+ * Unpack a RPC version 1 definition.  This can be used for introspection or
+ * validation.
+ *
+ * @param packed      raw packed bytes
+ * @param packed_len  length of packed in bytes
+ * @param def         RPC version 1 definition (output)
+ * @return True if successfully unpacked, false otherwise.
+ */
+NT_Bool NT_UnpackRpcDefinition(const char* packed, size_t packed_len,
+                               struct NT_RpcDefinition* def);
+
+/**
+ * Pack RPC values as required for RPC version 1 definition messages.
+ *
+ * @param values      array of values to pack
+ * @param values_len  length of values
+ * @param packed_len  length of return value in bytes
+ * @return Raw packed bytes.  Use C standard library std::free() to release.
+ */
+char* NT_PackRpcValues(const struct NT_Value** values, size_t values_len,
+                       size_t* packed_len);
+
+/**
+ * Unpack RPC values as required for RPC version 1 definition messages.
+ *
+ * @param packed      raw packed bytes
+ * @param packed_len  length of packed in bytes
+ * @param types       array of data types (as provided in the RPC definition)
+ * @param types_len   length of types
+ * @return Array of NT_Value's.
+ */
+struct NT_Value** NT_UnpackRpcValues(const char* packed, size_t packed_len,
+                                     const enum NT_Type* types,
+                                     size_t types_len);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_network_cfunc Client/Server Functions
+ * @{
+ */
+
+/**
+ * Set the network identity of this node.
+ * This is the name used during the initial connection handshake, and is
+ * visible through NT_ConnectionInfo on the remote node.
+ *
+ * @param inst      instance handle
+ * @param name      identity to advertise
+ * @param name_len  length of name in bytes
+ */
+void NT_SetNetworkIdentity(NT_Inst inst, const char* name, size_t name_len);
+
+/**
+ * Get the current network mode.
+ *
+ * @param inst  instance handle
+ * @return Bitmask of NT_NetworkMode.
+ */
+unsigned int NT_GetNetworkMode(NT_Inst inst);
+
+/**
+ * Starts a server using the specified filename, listening address, and port.
+ *
+ * @param inst              instance handle
+ * @param persist_filename  the name of the persist file to use (UTF-8 string,
+ *                          null terminated)
+ * @param listen_address    the address to listen on, or null to listen on any
+ *                          address. (UTF-8 string, null terminated)
+ * @param port              port to communicate over.
+ */
+void NT_StartServer(NT_Inst inst, const char* persist_filename,
+                    const char* listen_address, unsigned int port);
+
+/**
+ * Stops the server if it is running.
+ *
+ * @param inst  instance handle
+ */
+void NT_StopServer(NT_Inst inst);
+
+/**
+ * Starts a client.  Use NT_SetServer to set the server name and port.
+ *
+ * @param inst  instance handle
+ */
+void NT_StartClientNone(NT_Inst inst);
+
+/**
+ * Starts a client using the specified server and port
+ *
+ * @param inst        instance handle
+ * @param server_name server name (UTF-8 string, null terminated)
+ * @param port        port to communicate over
+ */
+void NT_StartClient(NT_Inst inst, const char* server_name, unsigned int port);
+
+/**
+ * Starts a client using the specified (server, port) combinations.  The
+ * client will attempt to connect to each server in round robin fashion.
+ *
+ * @param inst         instance handle
+ * @param count        length of the server_names and ports arrays
+ * @param server_names array of server names (each a UTF-8 string, null
+ *                     terminated)
+ * @param ports        array of ports to communicate over (one for each server)
+ */
+void NT_StartClientMulti(NT_Inst inst, size_t count, const char** server_names,
+                         const unsigned int* ports);
+
+/**
+ * Starts a client using commonly known robot addresses for the specified team.
+ *
+ * @param inst        instance handle
+ * @param team        team number
+ * @param port        port to communicate over
+ */
+void NT_StartClientTeam(NT_Inst inst, unsigned int team, unsigned int port);
+
+/**
+ * Stops the client if it is running.
+ *
+ * @param inst  instance handle
+ */
+void NT_StopClient(NT_Inst inst);
+
+/**
+ * Sets server address and port for client (without restarting client).
+ *
+ * @param inst        instance handle
+ * @param server_name server name (UTF-8 string, null terminated)
+ * @param port        port to communicate over
+ */
+void NT_SetServer(NT_Inst inst, const char* server_name, unsigned int port);
+
+/**
+ * Sets server addresses for client (without restarting client).
+ * The client will attempt to connect to each server in round robin fashion.
+ *
+ * @param inst         instance handle
+ * @param count        length of the server_names and ports arrays
+ * @param server_names array of server names (each a UTF-8 string, null
+ *                     terminated)
+ * @param ports        array of ports to communicate over (one for each server)
+ */
+void NT_SetServerMulti(NT_Inst inst, size_t count, const char** server_names,
+                       const unsigned int* ports);
+
+/**
+ * Sets server addresses and port for client (without restarting client).
+ * Connects using commonly known robot addresses for the specified team.
+ *
+ * @param inst        instance handle
+ * @param team        team number
+ * @param port        port to communicate over
+ */
+void NT_SetServerTeam(NT_Inst inst, unsigned int team, unsigned int port);
+
+/**
+ * Starts requesting server address from Driver Station.
+ * This connects to the Driver Station running on localhost to obtain the
+ * server IP address.
+ *
+ * @param inst  instance handle
+ * @param port server port to use in combination with IP from DS
+ */
+void NT_StartDSClient(NT_Inst inst, unsigned int port);
+
+/**
+ * Stops requesting server address from Driver Station.
+ *
+ * @param inst  instance handle
+ */
+void NT_StopDSClient(NT_Inst inst);
+
+/**
+ * Set the periodic update rate.
+ * Sets how frequently updates are sent to other nodes over the network.
+ *
+ * @param inst      instance handle
+ * @param interval  update interval in seconds (range 0.01 to 1.0)
+ */
+void NT_SetUpdateRate(NT_Inst inst, double interval);
+
+/**
+ * Flush Entries.
+ *
+ * Forces an immediate flush of all local entry changes to network.
+ * Normally this is done on a regularly scheduled interval (see
+ * NT_SetUpdateRate()).
+ *
+ * Note: flushes are rate limited to avoid excessive network traffic.  If
+ * the time between calls is too short, the flush will occur after the minimum
+ * time elapses (rather than immediately).
+ *
+ * @param inst      instance handle
+ */
+void NT_Flush(NT_Inst inst);
+
+/**
+ * Get information on the currently established network connections.
+ * If operating as a client, this will return either zero or one values.
+ *
+ * @param inst  instance handle
+ * @param count returns the number of elements in the array
+ * @return      array of connection information
+ *
+ * It is the caller's responsibility to free the array. The
+ * NT_DisposeConnectionInfoArray function is useful for this purpose.
+ */
+struct NT_ConnectionInfo* NT_GetConnections(NT_Inst inst, size_t* count);
+
+/**
+ * Return whether or not the instance is connected to another node.
+ *
+ * @param inst  instance handle
+ * @return True if connected.
+ */
+NT_Bool NT_IsConnected(NT_Inst inst);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_file_cfunc File Save/Load Functions
+ * @{
+ */
+
+/**
+ * Save persistent values to a file.  The server automatically does this,
+ * but this function provides a way to save persistent values in the same
+ * format to a file on either a client or a server.
+ *
+ * @param inst      instance handle
+ * @param filename  filename
+ * @return error string, or NULL if successful
+ */
+const char* NT_SavePersistent(NT_Inst inst, const char* filename);
+
+/**
+ * Load persistent values from a file.  The server automatically does this
+ * at startup, but this function provides a way to restore persistent values
+ * in the same format from a file at any time on either a client or a server.
+ *
+ * @param inst      instance handle
+ * @param filename  filename
+ * @param warn      callback function for warnings
+ * @return error string, or NULL if successful
+ */
+const char* NT_LoadPersistent(NT_Inst inst, const char* filename,
+                              void (*warn)(size_t line, const char* msg));
+
+/**
+ * Save table values to a file.  The file format used is identical to
+ * that used for SavePersistent.
+ *
+ * @param inst        instance handle
+ * @param filename    filename
+ * @param prefix      save only keys starting with this prefix
+ * @param prefix_len  length of prefix in bytes
+ * @return error string, or nullptr if successful
+ */
+const char* NT_SaveEntries(NT_Inst inst, const char* filename,
+                           const char* prefix, size_t prefix_len);
+
+/**
+ * Load table values from a file.  The file format used is identical to
+ * that used for SavePersistent / LoadPersistent.
+ *
+ * @param inst        instance handle
+ * @param filename    filename
+ * @param prefix      load only keys starting with this prefix
+ * @param prefix_len  length of prefix in bytes
+ * @param warn        callback function for warnings
+ * @return error string, or nullptr if successful
+ */
+const char* NT_LoadEntries(NT_Inst inst, const char* filename,
+                           const char* prefix, size_t prefix_len,
+                           void (*warn)(size_t line, const char* msg));
+
+/** @} */
+
+/**
+ * @defgroup ntcore_utility_cfunc Utility Functions
+ * @{
+ */
+
+/**
+ * Frees value memory.
+ *
+ * @param value   value to free
+ */
+void NT_DisposeValue(struct NT_Value* value);
+
+/**
+ * Initializes a NT_Value.
+ * Sets type to NT_UNASSIGNED and clears rest of struct.
+ *
+ * @param value value to initialize
+ */
+void NT_InitValue(struct NT_Value* value);
+
+/**
+ * Frees string memory.
+ *
+ * @param str   string to free
+ */
+void NT_DisposeString(struct NT_String* str);
+
+/**
+ * Initializes a NT_String.
+ * Sets length to zero and pointer to null.
+ *
+ * @param str   string to initialize
+ */
+void NT_InitString(struct NT_String* str);
+
+/**
+ * Disposes an entry handle array.
+ *
+ * @param arr   pointer to the array to dispose
+ * @param count number of elements in the array
+ */
+void NT_DisposeEntryArray(NT_Entry* arr, size_t count);
+
+/**
+ * Disposes a connection info array.
+ *
+ * @param arr   pointer to the array to dispose
+ * @param count number of elements in the array
+ */
+void NT_DisposeConnectionInfoArray(struct NT_ConnectionInfo* arr, size_t count);
+
+/**
+ * Disposes an entry info array.
+ *
+ * @param arr   pointer to the array to dispose
+ * @param count number of elements in the array
+ */
+void NT_DisposeEntryInfoArray(struct NT_EntryInfo* arr, size_t count);
+
+/**
+ * Disposes a single entry info (as returned by NT_GetEntryInfoHandle).
+ *
+ * @param info  pointer to the info to dispose
+ */
+void NT_DisposeEntryInfo(struct NT_EntryInfo* info);
+
+/**
+ * Disposes a Rpc Definition structure.
+ *
+ * @param def  pointer to the struct to dispose
+ */
+void NT_DisposeRpcDefinition(struct NT_RpcDefinition* def);
+
+/**
+ * Disposes a Rpc Answer array.
+ *
+ * @param arr   pointer to the array to dispose
+ * @param count number of elements in the array
+ */
+void NT_DisposeRpcAnswerArray(struct NT_RpcAnswer* arr, size_t count);
+
+/**
+ * Disposes a Rpc Answer structure.
+ *
+ * @param answer     pointer to the struct to dispose
+ */
+void NT_DisposeRpcAnswer(struct NT_RpcAnswer* answer);
+
+/**
+ * Disposes an entry notification array.
+ *
+ * @param arr   pointer to the array to dispose
+ * @param count number of elements in the array
+ */
+void NT_DisposeEntryNotificationArray(struct NT_EntryNotification* arr,
+                                      size_t count);
+
+/**
+ * Disposes a single entry notification.
+ *
+ * @param info  pointer to the info to dispose
+ */
+void NT_DisposeEntryNotification(struct NT_EntryNotification* info);
+
+/**
+ * Disposes a connection notification array.
+ *
+ * @param arr   pointer to the array to dispose
+ * @param count number of elements in the array
+ */
+void NT_DisposeConnectionNotificationArray(
+    struct NT_ConnectionNotification* arr, size_t count);
+
+/**
+ * Disposes a single connection notification.
+ *
+ * @param info  pointer to the info to dispose
+ */
+void NT_DisposeConnectionNotification(struct NT_ConnectionNotification* info);
+
+/**
+ * Disposes a log message array.
+ *
+ * @param arr   pointer to the array to dispose
+ * @param count number of elements in the array
+ */
+void NT_DisposeLogMessageArray(struct NT_LogMessage* arr, size_t count);
+
+/**
+ * Disposes a single log message.
+ *
+ * @param info  pointer to the info to dispose
+ */
+void NT_DisposeLogMessage(struct NT_LogMessage* info);
+
+/**
+ * Returns monotonic current time in 1 us increments.
+ * This is the same time base used for entry and connection timestamps.
+ * This function is a compatibility wrapper around WPI_Now().
+ *
+ * @return Timestamp
+ */
+uint64_t NT_Now(void);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_logger_cfunc Logger Functions
+ * @{
+ */
+
+/**
+ * Log function.
+ *
+ * @param data    data pointer passed to NT_AddLogger()
+ * @param msg     message information
+ */
+typedef void (*NT_LogFunc)(void* data, const struct NT_LogMessage* msg);
+
+/**
+ * Add logger callback function.  By default, log messages are sent to stderr;
+ * this function sends log messages to the provided callback function instead.
+ * The callback function will only be called for log messages with level
+ * greater than or equal to min_level and less than or equal to max_level;
+ * messages outside this range will be silently ignored.
+ *
+ * @param inst        instance handle
+ * @param data        data pointer to pass to func
+ * @param func        log callback function
+ * @param min_level   minimum log level
+ * @param max_level   maximum log level
+ * @return Logger handle
+ */
+NT_Logger NT_AddLogger(NT_Inst inst, void* data, NT_LogFunc func,
+                       unsigned int min_level, unsigned int max_level);
+
+/**
+ * Create a log poller.  A poller provides a single queue of poll events.
+ * The returned handle must be destroyed with NT_DestroyLoggerPoller().
+ *
+ * @param inst      instance handle
+ * @return poller handle
+ */
+NT_LoggerPoller NT_CreateLoggerPoller(NT_Inst inst);
+
+/**
+ * Destroy a log poller.  This will abort any blocked polling call and prevent
+ * additional events from being generated for this poller.
+ *
+ * @param poller    poller handle
+ */
+void NT_DestroyLoggerPoller(NT_LoggerPoller poller);
+
+/**
+ * Set the log level for a log poller.  Events will only be generated for
+ * log messages with level greater than or equal to min_level and less than or
+ * equal to max_level; messages outside this range will be silently ignored.
+ *
+ * @param poller        poller handle
+ * @param min_level     minimum log level
+ * @param max_level     maximum log level
+ * @return Logger handle
+ */
+NT_Logger NT_AddPolledLogger(NT_LoggerPoller poller, unsigned int min_level,
+                             unsigned int max_level);
+
+/**
+ * Get the next log event.  This blocks until the next log occurs.
+ *
+ * @param poller    poller handle
+ * @param len       length of returned array (output)
+ * @return Array of information on the log events.  Only returns NULL if an
+ *         error occurred (e.g. the instance was invalid or is shutting down).
+ */
+struct NT_LogMessage* NT_PollLogger(NT_LoggerPoller poller, size_t* len);
+
+/**
+ * Get the next log event.  This blocks until the next log occurs or it times
+ * out.
+ *
+ * @param poller      poller handle
+ * @param len         length of returned array (output)
+ * @param timeout     timeout, in seconds
+ * @param timed_out   true if the timeout period elapsed (output)
+ * @return Array of information on the log events.  If NULL is returned and
+ *         timed_out is also false, an error occurred (e.g. the instance was
+ *         invalid or is shutting down).
+ */
+struct NT_LogMessage* NT_PollLoggerTimeout(NT_LoggerPoller poller, size_t* len,
+                                           double timeout, NT_Bool* timed_out);
+
+/**
+ * Cancel a PollLogger call.  This wakes up a call to PollLogger for this
+ * poller and causes it to immediately return an empty array.
+ *
+ * @param poller  poller handle
+ */
+void NT_CancelPollLogger(NT_LoggerPoller poller);
+
+/**
+ * Remove a logger.
+ *
+ * @param logger Logger handle to remove
+ */
+void NT_RemoveLogger(NT_Logger logger);
+
+/**
+ * Wait for the incoming log event queue to be empty.  This is primarily useful
+ * for deterministic testing.  This blocks until either the log event
+ * queue is empty (e.g. there are no more events that need to be passed along
+ * to callbacks or poll queues) or the timeout expires.
+ *
+ * @param inst      instance handle
+ * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+ *                  or a negative value to block indefinitely
+ * @return False if timed out, otherwise true.
+ */
+NT_Bool NT_WaitForLoggerQueue(NT_Inst inst, double timeout);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_interop_cfunc Interop Utility Functions
+ * @{
+ */
+
+/**
+ * @defgroup ntcore_memoryallocators_cfunc Memory Allocators
+ * @{
+ */
+
+/**
+ * Allocates an array of chars.
+ * Note that the size is the number of elements, and not the
+ * specific number of bytes to allocate. That is calculated internally.
+ *
+ * @param size  the number of elements the array will contain
+ * @return      the allocated char array
+ *
+ * After use, the array should be freed using the NT_FreeCharArray()
+ * function.
+ */
+char* NT_AllocateCharArray(size_t size);
+
+/**
+ * Allocates an array of booleans.
+ * Note that the size is the number of elements, and not the
+ * specific number of bytes to allocate. That is calculated internally.
+ *
+ * @param size  the number of elements the array will contain
+ * @return      the allocated boolean array
+ *
+ * After use, the array should be freed using the NT_FreeBooleanArray()
+ * function.
+ */
+NT_Bool* NT_AllocateBooleanArray(size_t size);
+
+/**
+ * Allocates an array of doubles.
+ * Note that the size is the number of elements, and not the
+ * specific number of bytes to allocate. That is calculated internally.
+ *
+ * @param size  the number of elements the array will contain
+ * @return      the allocated double array
+ *
+ * After use, the array should be freed using the NT_FreeDoubleArray()
+ * function.
+ */
+double* NT_AllocateDoubleArray(size_t size);
+
+/**
+ * Allocates an array of NT_Strings.
+ * Note that the size is the number of elements, and not the
+ * specific number of bytes to allocate. That is calculated internally.
+ *
+ * @param size  the number of elements the array will contain
+ * @return      the allocated NT_String array
+ *
+ * After use, the array should be freed using the NT_FreeStringArray()
+ * function.
+ */
+struct NT_String* NT_AllocateStringArray(size_t size);
+
+/**
+ * Frees an array of chars.
+ *
+ * @param v_boolean  pointer to the char array to free
+ */
+void NT_FreeCharArray(char* v_char);
+
+/**
+ * Frees an array of doubles.
+ *
+ * @param v_boolean  pointer to the double array to free
+ */
+void NT_FreeDoubleArray(double* v_double);
+
+/**
+ * Frees an array of booleans.
+ *
+ * @param v_boolean  pointer to the boolean array to free
+ */
+void NT_FreeBooleanArray(NT_Bool* v_boolean);
+
+/**
+ * Frees an array of NT_Strings.
+ *
+ * @param v_string  pointer to the string array to free
+ * @param arr_size  size of the string array to free
+ *
+ * Note that the individual NT_Strings in the array should NOT be
+ * freed before calling this. This function will free all the strings
+ * individually.
+ */
+void NT_FreeStringArray(struct NT_String* v_string, size_t arr_size);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_typedgetters_cfunc Typed Getters
+ * @{
+ */
+
+/**
+ * Returns the type of an NT_Value struct.
+ * Note that one of the type options is "unassigned".
+ *
+ * @param value  The NT_Value struct to get the type from.
+ * @return       The type of the value, or unassigned if null.
+ */
+enum NT_Type NT_GetValueType(const struct NT_Value* value);
+
+/**
+ * Returns the boolean from the NT_Value.
+ * If the NT_Value is null, or is assigned to a different type, returns 0.
+ *
+ * @param value       NT_Value struct to get the boolean from
+ * @param last_change returns time in ms since the last change in the value
+ * @param v_boolean   returns the boolean assigned to the name
+ * @return            1 if successful, or 0 if value is null or not a boolean
+ */
+NT_Bool NT_GetValueBoolean(const struct NT_Value* value, uint64_t* last_change,
+                           NT_Bool* v_boolean);
+
+/**
+ * Returns the double from the NT_Value.
+ * If the NT_Value is null, or is assigned to a different type, returns 0.
+ *
+ * @param value       NT_Value struct to get the double from
+ * @param last_change returns time in ms since the last change in the value
+ * @param v_double    returns the boolean assigned to the name
+ * @return            1 if successful, or 0 if value is null or not a double
+ */
+NT_Bool NT_GetValueDouble(const struct NT_Value* value, uint64_t* last_change,
+                          double* v_double);
+
+/**
+ * Returns a copy of the string from the NT_Value.
+ * If the NT_Value is null, or is assigned to a different type, returns 0.
+ *
+ * @param value       NT_Value struct to get the string from
+ * @param last_change returns time in ms since the last change in the value
+ * @param str_len     returns the length of the string
+ * @return            pointer to the string (UTF-8), or null if error
+ *
+ * It is the caller's responsibility to free the string once its no longer
+ * needed. The NT_FreeCharArray() function is useful for this purpose. The
+ * returned string is a copy of the string in the value, and must be freed
+ * separately.
+ */
+char* NT_GetValueString(const struct NT_Value* value, uint64_t* last_change,
+                        size_t* str_len);
+
+/**
+ * Returns a copy of the raw value from the NT_Value.
+ * If the NT_Value is null, or is assigned to a different type, returns null.
+ *
+ * @param value       NT_Value struct to get the string from
+ * @param last_change returns time in ms since the last change in the value
+ * @param raw_len     returns the length of the string
+ * @return            pointer to the raw value (UTF-8), or null if error
+ *
+ * It is the caller's responsibility to free the raw value once its no longer
+ * needed. The NT_FreeCharArray() function is useful for this purpose. The
+ * returned string is a copy of the string in the value, and must be freed
+ * separately.
+ */
+char* NT_GetValueRaw(const struct NT_Value* value, uint64_t* last_change,
+                     size_t* raw_len);
+
+/**
+ * Returns a copy of the boolean array from the NT_Value.
+ * If the NT_Value is null, or is assigned to a different type, returns null.
+ *
+ * @param value       NT_Value struct to get the boolean array from
+ * @param last_change returns time in ms since the last change in the value
+ * @param arr_size    returns the number of elements in the array
+ * @return            pointer to the boolean array, or null if error
+ *
+ * It is the caller's responsibility to free the array once its no longer
+ * needed. The NT_FreeBooleanArray() function is useful for this purpose.
+ * The returned array is a copy of the array in the value, and must be
+ * freed separately.
+ */
+NT_Bool* NT_GetValueBooleanArray(const struct NT_Value* value,
+                                 uint64_t* last_change, size_t* arr_size);
+
+/**
+ * Returns a copy of the double array from the NT_Value.
+ * If the NT_Value is null, or is assigned to a different type, returns null.
+ *
+ * @param value       NT_Value struct to get the double array from
+ * @param last_change returns time in ms since the last change in the value
+ * @param arr_size    returns the number of elements in the array
+ * @return            pointer to the double array, or null if error
+ *
+ * It is the caller's responsibility to free the array once its no longer
+ * needed. The NT_FreeDoubleArray() function is useful for this purpose.
+ * The returned array is a copy of the array in the value, and must be
+ * freed separately.
+ */
+double* NT_GetValueDoubleArray(const struct NT_Value* value,
+                               uint64_t* last_change, size_t* arr_size);
+
+/**
+ * Returns a copy of the NT_String array from the NT_Value.
+ * If the NT_Value is null, or is assigned to a different type, returns null.
+ *
+ * @param value       NT_Value struct to get the NT_String array from
+ * @param last_change returns time in ms since the last change in the value
+ * @param arr_size    returns the number of elements in the array
+ * @return            pointer to the NT_String array, or null if error
+ *
+ * It is the caller's responsibility to free the array once its no longer
+ * needed. The NT_FreeStringArray() function is useful for this purpose.
+ * The returned array is a copy of the array in the value, and must be
+ * freed seperately. Note that the individual NT_Strings should not be freed,
+ * but the entire array should be freed at once. The NT_FreeStringArray()
+ * function will free all the NT_Strings.
+ */
+struct NT_String* NT_GetValueStringArray(const struct NT_Value* value,
+                                         uint64_t* last_change,
+                                         size_t* arr_size);
+
+/**
+ * Returns the boolean currently assigned to the entry name.
+ * If the entry name is not currently assigned, or is assigned to a
+ * different type, returns 0.
+ *
+ * @param entry       entry handle
+ * @param last_change returns time in ms since the last change in the value
+ * @param v_boolean   returns the boolean assigned to the name
+ * @return            1 if successful, or 0 if value is unassigned or not a
+ *                    boolean
+ */
+NT_Bool NT_GetEntryBoolean(NT_Entry entry, uint64_t* last_change,
+                           NT_Bool* v_boolean);
+
+/**
+ * Returns the double currently assigned to the entry name.
+ * If the entry name is not currently assigned, or is assigned to a
+ * different type, returns 0.
+ *
+ * @param entry       entry handle
+ * @param last_change returns time in ms since the last change in the value
+ * @param v_double    returns the double assigned to the name
+ * @return            1 if successful, or 0 if value is unassigned or not a
+ *                    double
+ */
+NT_Bool NT_GetEntryDouble(NT_Entry entry, uint64_t* last_change,
+                          double* v_double);
+
+/**
+ * Returns a copy of the string assigned to the entry name.
+ * If the entry name is not currently assigned, or is assigned to a
+ * different type, returns null.
+ *
+ * @param entry       entry handle
+ * @param last_change returns time in ms since the last change in the value
+ * @param str_len     returns the length of the string
+ * @return            pointer to the string (UTF-8), or null if error
+ *
+ * It is the caller's responsibility to free the string once its no longer
+ * needed. The NT_FreeCharArray() function is useful for this purpose.
+ */
+char* NT_GetEntryString(NT_Entry entry, uint64_t* last_change, size_t* str_len);
+
+/**
+ * Returns a copy of the raw value assigned to the entry name.
+ * If the entry name is not currently assigned, or is assigned to a
+ * different type, returns null.
+ *
+ * @param entry       entry handle
+ * @param last_change returns time in ms since the last change in the value
+ * @param raw_len     returns the length of the string
+ * @return            pointer to the raw value (UTF-8), or null if error
+ *
+ * It is the caller's responsibility to free the raw value once its no longer
+ * needed. The NT_FreeCharArray() function is useful for this purpose.
+ */
+char* NT_GetEntryRaw(NT_Entry entry, uint64_t* last_change, size_t* raw_len);
+
+/**
+ * Returns a copy of the boolean array assigned to the entry name.
+ * If the entry name is not currently assigned, or is assigned to a
+ * different type, returns null.
+ *
+ * @param entry       entry handle
+ * @param last_change returns time in ms since the last change in the value
+ * @param arr_size    returns the number of elements in the array
+ * @return            pointer to the boolean array, or null if error
+ *
+ * It is the caller's responsibility to free the array once its no longer
+ * needed. The NT_FreeBooleanArray() function is useful for this purpose.
+ */
+NT_Bool* NT_GetEntryBooleanArray(NT_Entry entry, uint64_t* last_change,
+                                 size_t* arr_size);
+
+/**
+ * Returns a copy of the double array assigned to the entry name.
+ * If the entry name is not currently assigned, or is assigned to a
+ * different type, returns null.
+ *
+ * @param entry       entry handle
+ * @param last_change returns time in ms since the last change in the value
+ * @param arr_size    returns the number of elements in the array
+ * @return            pointer to the double array, or null if error
+ *
+ * It is the caller's responsibility to free the array once its no longer
+ * needed. The NT_FreeDoubleArray() function is useful for this purpose.
+ */
+double* NT_GetEntryDoubleArray(NT_Entry entry, uint64_t* last_change,
+                               size_t* arr_size);
+
+/**
+ * Returns a copy of the NT_String array assigned to the entry name.
+ * If the entry name is not currently assigned, or is assigned to a
+ * different type, returns null.
+ *
+ * @param entry       entry handle
+ * @param last_change returns time in ms since the last change in the value
+ * @param arr_size    returns the number of elements in the array
+ * @return            pointer to the NT_String array, or null if error
+ *
+ * It is the caller's responsibility to free the array once its no longer
+ * needed. The NT_FreeStringArray() function is useful for this purpose. Note
+ * that the individual NT_Strings should not be freed, but the entire array
+ * should be freed at once. The NT_FreeStringArray() function will free all the
+ * NT_Strings.
+ */
+struct NT_String* NT_GetEntryStringArray(NT_Entry entry, uint64_t* last_change,
+                                         size_t* arr_size);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_setdefault_cfunc Set Default Values
+ * @{
+ */
+
+/** Set Default Entry Boolean.
+ * Sets the default for the specified key to be a boolean.
+ * If key exists with same type, does not set value. Otherwise
+ * sets value to the default.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param default_boolean     value to be set if name does not exist
+ * @return 0 on error (value not set), 1 on success
+ */
+NT_Bool NT_SetDefaultEntryBoolean(NT_Entry entry, uint64_t time,
+                                  NT_Bool default_boolean);
+
+/** Set Default Entry Double.
+ * Sets the default for the specified key.
+ * If key exists with same type, does not set value. Otherwise
+ * sets value to the default.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param default_double     value to be set if name does not exist
+ * @return 0 on error (value not set), 1 on success
+ */
+NT_Bool NT_SetDefaultEntryDouble(NT_Entry entry, uint64_t time,
+                                 double default_double);
+
+/** Set Default Entry String.
+ * Sets the default for the specified key.
+ * If key exists with same type, does not set value. Otherwise
+ * sets value to the default.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param default_value     value to be set if name does not exist
+ * @param default_len       length of value
+ * @return 0 on error (value not set), 1 on success
+ */
+NT_Bool NT_SetDefaultEntryString(NT_Entry entry, uint64_t time,
+                                 const char* default_value, size_t default_len);
+
+/** Set Default Entry Raw.
+ * Sets the default for the specified key.
+ * If key exists with same type, does not set value. Otherwise
+ * sets value to the default.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param default_value     value to be set if name does not exist
+ * @param default_len       length of value array
+ * @return 0 on error (value not set), 1 on success
+ */
+NT_Bool NT_SetDefaultEntryRaw(NT_Entry entry, uint64_t time,
+                              const char* default_value, size_t default_len);
+
+/** Set Default Entry Boolean Array.
+ * Sets the default for the specified key.
+ * If key exists with same type, does not set value. Otherwise
+ * sets value to the default.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param default_value     value to be set if name does not exist
+ * @param default_size      size of value array
+ * @return 0 on error (value not set), 1 on success
+ */
+NT_Bool NT_SetDefaultEntryBooleanArray(NT_Entry entry, uint64_t time,
+                                       const int* default_value,
+                                       size_t default_size);
+
+/** Set Default Entry Double Array.
+ * Sets the default for the specified key.
+ * If key exists with same type, does not set value. Otherwise
+ * sets value to the default.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param default_value     value to be set if name does not exist
+ * @param default_size      size of value array
+ * @return 0 on error (value not set), 1 on success
+ */
+NT_Bool NT_SetDefaultEntryDoubleArray(NT_Entry entry, uint64_t time,
+                                      const double* default_value,
+                                      size_t default_size);
+
+/** Set Default Entry String Array.
+ * Sets the default for the specified key.
+ * If key exists with same type, does not set value. Otherwise
+ * sets value to the default.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param default_value     value to be set if name does not exist
+ * @param default_size      size of value array
+ * @return 0 on error (value not set), 1 on success
+ */
+NT_Bool NT_SetDefaultEntryStringArray(NT_Entry entry, uint64_t time,
+                                      const struct NT_String* default_value,
+                                      size_t default_size);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_valuesetters_cfunc Entry Value Setters
+ * @{
+ */
+
+/** Set Entry Boolean
+ * Sets an entry boolean. If the entry name is not currently assigned to a
+ * boolean, returns error unless the force parameter is set.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param v_boolean boolean value to set
+ * @param force     1 to force the entry to get overwritten, otherwise 0
+ * @return          0 on error (type mismatch), 1 on success
+ */
+NT_Bool NT_SetEntryBoolean(NT_Entry entry, uint64_t time, NT_Bool v_boolean,
+                           NT_Bool force);
+
+/** Set Entry Double
+ * Sets an entry double. If the entry name is not currently assigned to a
+ * double, returns error unless the force parameter is set.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param v_double  double value to set
+ * @param force     1 to force the entry to get overwritten, otherwise 0
+ * @return          0 on error (type mismatch), 1 on success
+ */
+NT_Bool NT_SetEntryDouble(NT_Entry entry, uint64_t time, double v_double,
+                          NT_Bool force);
+
+/** Set Entry String
+ * Sets an entry string. If the entry name is not currently assigned to a
+ * string, returns error unless the force parameter is set.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param str       string to set (UTF-8 string)
+ * @param str_len   length of string to write in bytes
+ * @param force     1 to force the entry to get overwritten, otherwise 0
+ * @return          0 on error (type mismatch), 1 on success
+ */
+NT_Bool NT_SetEntryString(NT_Entry entry, uint64_t time, const char* str,
+                          size_t str_len, NT_Bool force);
+
+/** Set Entry Raw
+ * Sets the raw value of an entry. If the entry name is not currently assigned
+ * to a raw value, returns error unless the force parameter is set.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param raw       raw string to set (UTF-8 string)
+ * @param raw_len   length of raw string to write in bytes
+ * @param force     1 to force the entry to get overwritten, otherwise 0
+ * @return          0 on error (type mismatch), 1 on success
+ */
+NT_Bool NT_SetEntryRaw(NT_Entry entry, uint64_t time, const char* raw,
+                       size_t raw_len, NT_Bool force);
+
+/** Set Entry Boolean Array
+ * Sets an entry boolean array. If the entry name is not currently assigned to
+ * a boolean array, returns error unless the force parameter is set.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param arr       boolean array to write
+ * @param size      number of elements in the array
+ * @param force     1 to force the entry to get overwritten, otherwise 0
+ * @return          0 on error (type mismatch), 1 on success
+ */
+NT_Bool NT_SetEntryBooleanArray(NT_Entry entry, uint64_t time, const int* arr,
+                                size_t size, NT_Bool force);
+
+/** Set Entry Double Array
+ * Sets an entry double array. If the entry name is not currently assigned to
+ * a double array, returns error unless the force parameter is set.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param arr       double array to write
+ * @param size      number of elements in the array
+ * @param force     1 to force the entry to get overwritten, otherwise 0
+ * @return          0 on error (type mismatch), 1 on success
+ */
+NT_Bool NT_SetEntryDoubleArray(NT_Entry entry, uint64_t time, const double* arr,
+                               size_t size, NT_Bool force);
+
+/** Set Entry String Array
+ * Sets an entry string array. If the entry name is not currently assigned to
+ * a string array, returns error unless the force parameter is set.
+ *
+ * @param entry     entry handle
+ * @param time      timestamp
+ * @param arr       NT_String array to write
+ * @param size      number of elements in the array
+ * @param force     1 to force the entry to get overwritten, otherwise 0
+ * @return          0 on error (type mismatch), 1 on success
+ */
+NT_Bool NT_SetEntryStringArray(NT_Entry entry, uint64_t time,
+                               const struct NT_String* arr, size_t size,
+                               NT_Bool force);
+
+/** @} */
+/** @} */
+/** @} */
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // NTCORE_NTCORE_C_H_
diff --git a/ntcore/src/main/native/include/ntcore_cpp.h b/ntcore/src/main/native/include/ntcore_cpp.h
new file mode 100644
index 0000000..56cb5af
--- /dev/null
+++ b/ntcore/src/main/native/include/ntcore_cpp.h
@@ -0,0 +1,1600 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NTCORE_CPP_H_
+#define NTCORE_NTCORE_CPP_H_
+
+#include <stdint.h>
+
+#include <cassert>
+#include <functional>
+#include <memory>
+#include <string>
+#include <thread>
+#include <utility>
+#include <vector>
+
+#include <wpi/ArrayRef.h>
+#include <wpi/StringRef.h>
+#include <wpi/Twine.h>
+#include <wpi/deprecated.h>
+
+#include "networktables/NetworkTableValue.h"
+
+/** NetworkTables (ntcore) namespace */
+namespace nt {
+
+/**
+ * @defgroup ntcore_cpp_handle_api ntcore C++ API
+ *
+ * Handle-based interface for C++.
+ *
+ * @{
+ */
+
+using wpi::ArrayRef;
+using wpi::StringRef;
+using wpi::Twine;
+
+/** NetworkTables Entry Information */
+struct EntryInfo {
+  /** Entry handle */
+  NT_Entry entry;
+
+  /** Entry name */
+  std::string name;
+
+  /** Entry type */
+  NT_Type type;
+
+  /** Entry flags */
+  unsigned int flags;
+
+  /** Timestamp of last change to entry (type or value). */
+  uint64_t last_change;
+
+  friend void swap(EntryInfo& first, EntryInfo& second) {
+    using std::swap;
+    swap(first.entry, second.entry);
+    swap(first.name, second.name);
+    swap(first.type, second.type);
+    swap(first.flags, second.flags);
+    swap(first.last_change, second.last_change);
+  }
+};
+
+/** NetworkTables Connection Information */
+struct ConnectionInfo {
+  /**
+   * The remote identifier (as set on the remote node by
+   * NetworkTableInstance::SetNetworkIdentity() or nt::SetNetworkIdentity()).
+   */
+  std::string remote_id;
+
+  /** The IP address of the remote node. */
+  std::string remote_ip;
+
+  /** The port number of the remote node. */
+  unsigned int remote_port;
+
+  /**
+   * The last time any update was received from the remote node (same scale as
+   * returned by nt::Now()).
+   */
+  uint64_t last_update;
+
+  /**
+   * The protocol version being used for this connection.  This in protocol
+   * layer format, so 0x0200 = 2.0, 0x0300 = 3.0).
+   */
+  unsigned int protocol_version;
+
+  friend void swap(ConnectionInfo& first, ConnectionInfo& second) {
+    using std::swap;
+    swap(first.remote_id, second.remote_id);
+    swap(first.remote_ip, second.remote_ip);
+    swap(first.remote_port, second.remote_port);
+    swap(first.last_update, second.last_update);
+    swap(first.protocol_version, second.protocol_version);
+  }
+};
+
+/** NetworkTables RPC Version 1 Definition Parameter */
+struct RpcParamDef {
+  RpcParamDef() = default;
+  RpcParamDef(StringRef name_, std::shared_ptr<Value> def_value_)
+      : name(name_), def_value(def_value_) {}
+
+  std::string name;
+  std::shared_ptr<Value> def_value;
+};
+
+/** NetworkTables RPC Version 1 Definition Result */
+struct RpcResultDef {
+  RpcResultDef() = default;
+  RpcResultDef(StringRef name_, NT_Type type_) : name(name_), type(type_) {}
+
+  std::string name;
+  NT_Type type;
+};
+
+/** NetworkTables RPC Version 1 Definition */
+struct RpcDefinition {
+  unsigned int version;
+  std::string name;
+  std::vector<RpcParamDef> params;
+  std::vector<RpcResultDef> results;
+};
+
+/** NetworkTables Remote Procedure Call (Server Side) */
+class RpcAnswer {
+ public:
+  RpcAnswer() : entry(0), call(0) {}
+  RpcAnswer(NT_Entry entry_, NT_RpcCall call_, StringRef name_,
+            StringRef params_, const ConnectionInfo& conn_)
+      : entry(entry_), call(call_), name(name_), params(params_), conn(conn_) {}
+
+  /** Entry handle. */
+  NT_Entry entry;
+
+  /** Call handle. */
+  mutable NT_RpcCall call;
+
+  /** Entry name. */
+  std::string name;
+
+  /** Call raw parameters. */
+  std::string params;
+
+  /** Connection that called the RPC. */
+  ConnectionInfo conn;
+
+  /**
+   * Determines if the native handle is valid.
+   * @return True if the native handle is valid, false otherwise.
+   */
+  explicit operator bool() const { return call != 0; }
+
+  /**
+   * Post RPC response (return value) for a polled RPC.
+   * @param result  result raw data that will be provided to remote caller
+   * @return True if posting the response is valid, otherwise false
+   */
+  bool PostResponse(StringRef result) const;
+
+  friend void swap(RpcAnswer& first, RpcAnswer& second) {
+    using std::swap;
+    swap(first.entry, second.entry);
+    swap(first.call, second.call);
+    swap(first.name, second.name);
+    swap(first.params, second.params);
+    swap(first.conn, second.conn);
+  }
+};
+
+/** NetworkTables Entry Notification */
+class EntryNotification {
+ public:
+  EntryNotification() : listener(0), entry(0) {}
+  EntryNotification(NT_EntryListener listener_, NT_Entry entry_,
+                    StringRef name_, std::shared_ptr<Value> value_,
+                    unsigned int flags_)
+      : listener(listener_),
+        entry(entry_),
+        name(name_),
+        value(value_),
+        flags(flags_) {}
+
+  /** Listener that was triggered. */
+  NT_EntryListener listener;
+
+  /** Entry handle. */
+  NT_Entry entry;
+
+  /** Entry name. */
+  std::string name;
+
+  /** The new value. */
+  std::shared_ptr<Value> value;
+
+  /**
+   * Update flags.  For example, NT_NOTIFY_NEW if the key did not previously
+   * exist.
+   */
+  unsigned int flags;
+
+  friend void swap(EntryNotification& first, EntryNotification& second) {
+    using std::swap;
+    swap(first.listener, second.listener);
+    swap(first.entry, second.entry);
+    swap(first.name, second.name);
+    swap(first.value, second.value);
+    swap(first.flags, second.flags);
+  }
+};
+
+/** NetworkTables Connection Notification */
+class ConnectionNotification {
+ public:
+  ConnectionNotification() : listener(0), connected(false) {}
+  ConnectionNotification(NT_ConnectionListener listener_, bool connected_,
+                         const ConnectionInfo& conn_)
+      : listener(listener_), connected(connected_), conn(conn_) {}
+
+  /** Listener that was triggered. */
+  NT_ConnectionListener listener;
+
+  /** True if event is due to connection being established. */
+  bool connected = false;
+
+  /** Connection info. */
+  ConnectionInfo conn;
+
+  friend void swap(ConnectionNotification& first,
+                   ConnectionNotification& second) {
+    using std::swap;
+    swap(first.listener, second.listener);
+    swap(first.connected, second.connected);
+    swap(first.conn, second.conn);
+  }
+};
+
+/** NetworkTables log message. */
+class LogMessage {
+ public:
+  LogMessage() : logger(0), level(0), filename(""), line(0) {}
+  LogMessage(NT_Logger logger_, unsigned int level_, const char* filename_,
+             unsigned int line_, StringRef message_)
+      : logger(logger_),
+        level(level_),
+        filename(filename_),
+        line(line_),
+        message(message_) {}
+
+  /** The logger that generated the message. */
+  NT_Logger logger;
+
+  /** Log level of the message.  See NT_LogLevel. */
+  unsigned int level;
+
+  /** The filename of the source file that generated the message. */
+  const char* filename;
+
+  /** The line number in the source file that generated the message. */
+  unsigned int line;
+
+  /** The message. */
+  std::string message;
+
+  friend void swap(LogMessage& first, LogMessage& second) {
+    using std::swap;
+    swap(first.logger, second.logger);
+    swap(first.level, second.level);
+    swap(first.filename, second.filename);
+    swap(first.line, second.line);
+    swap(first.message, second.message);
+  }
+};
+
+/**
+ * @defgroup ntcore_instance_func Instance Functions
+ * @{
+ */
+
+/**
+ * Get default instance.
+ * This is the instance used by non-handle-taking functions.
+ *
+ * @return Instance handle
+ */
+NT_Inst GetDefaultInstance();
+
+/**
+ * Create an instance.
+ *
+ * @return Instance handle
+ */
+NT_Inst CreateInstance();
+
+/**
+ * Destroy an instance.
+ * The default instance cannot be destroyed.
+ *
+ * @param inst Instance handle
+ */
+void DestroyInstance(NT_Inst inst);
+
+/**
+ * Get instance handle from another handle.
+ *
+ * @param handle    entry/instance/etc. handle
+ * @return Instance handle
+ */
+NT_Inst GetInstanceFromHandle(NT_Handle handle);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_table_func Table Functions
+ * @{
+ */
+
+/**
+ * Get Entry Handle.
+ *
+ * @param inst      instance handle
+ * @param name      entry name (UTF-8 string)
+ * @return entry handle
+ */
+NT_Entry GetEntry(NT_Inst inst, const Twine& name);
+
+/**
+ * Get Entry Handles.
+ *
+ * Returns an array of entry handles.  The results are optionally
+ * filtered by string prefix and entry type to only return a subset of all
+ * entries.
+ *
+ * @param inst          instance handle
+ * @param prefix        entry name required prefix; only entries whose name
+ *                      starts with this string are returned
+ * @param types         bitmask of NT_Type values; 0 is treated specially
+ *                      as a "don't care"
+ * @return Array of entry handles.
+ */
+std::vector<NT_Entry> GetEntries(NT_Inst inst, const Twine& prefix,
+                                 unsigned int types);
+
+/**
+ * Gets the name of the specified entry.
+ * Returns an empty string if the handle is invalid.
+ *
+ * @param entry   entry handle
+ * @return Entry name
+ */
+std::string GetEntryName(NT_Entry entry);
+
+/**
+ * Gets the type for the specified entry, or unassigned if non existent.
+ *
+ * @param entry   entry handle
+ * @return Entry type
+ */
+NT_Type GetEntryType(NT_Entry entry);
+
+/**
+ * Gets the last time the entry was changed.
+ * Returns 0 if the handle is invalid.
+ *
+ * @param entry   entry handle
+ * @return Entry last change time
+ */
+uint64_t GetEntryLastChange(NT_Entry entry);
+
+/**
+ * Get Entry Value.
+ *
+ * Returns copy of current entry value.
+ * Note that one of the type options is "unassigned".
+ *
+ * @param name      entry name (UTF-8 string)
+ * @return entry value
+ */
+WPI_DEPRECATED("use NT_Entry function instead")
+std::shared_ptr<Value> GetEntryValue(StringRef name);
+
+/**
+ * Get Entry Value.
+ *
+ * Returns copy of current entry value.
+ * Note that one of the type options is "unassigned".
+ *
+ * @param entry     entry handle
+ * @return entry value
+ */
+std::shared_ptr<Value> GetEntryValue(NT_Entry entry);
+
+/**
+ * Set Default Entry Value
+ *
+ * Returns copy of current entry value if it exists.
+ * Otherwise, sets passed in value, and returns set value.
+ * Note that one of the type options is "unassigned".
+ *
+ * @param name      entry name (UTF-8 string)
+ * @param value     value to be set if name does not exist
+ * @return False on error (value not set), True on success
+ */
+WPI_DEPRECATED("use NT_Entry function instead")
+bool SetDefaultEntryValue(StringRef name, std::shared_ptr<Value> value);
+
+/**
+ * Set Default Entry Value
+ *
+ * Returns copy of current entry value if it exists.
+ * Otherwise, sets passed in value, and returns set value.
+ * Note that one of the type options is "unassigned".
+ *
+ * @param entry     entry handle
+ * @param value     value to be set if name does not exist
+ * @return False on error (value not set), True on success
+ */
+bool SetDefaultEntryValue(NT_Entry entry, std::shared_ptr<Value> value);
+
+/**
+ * Set Entry Value.
+ *
+ * Sets new entry value.  If type of new value differs from the type of the
+ * currently stored entry, returns error and does not update value.
+ *
+ * @param name      entry name (UTF-8 string)
+ * @param value     new entry value
+ * @return False on error (type mismatch), True on success
+ */
+WPI_DEPRECATED("use NT_Entry function instead")
+bool SetEntryValue(StringRef name, std::shared_ptr<Value> value);
+
+/**
+ * Set Entry Value.
+ *
+ * Sets new entry value.  If type of new value differs from the type of the
+ * currently stored entry, returns error and does not update value.
+ *
+ * @param entry     entry handle
+ * @param value     new entry value
+ * @return False on error (type mismatch), True on success
+ */
+bool SetEntryValue(NT_Entry entry, std::shared_ptr<Value> value);
+
+/**
+ * Set Entry Type and Value.
+ *
+ * Sets new entry value.  If type of new value differs from the type of the
+ * currently stored entry, the currently stored entry type is overridden
+ * (generally this will generate an Entry Assignment message).
+ *
+ * This is NOT the preferred method to update a value; generally
+ * SetEntryValue() should be used instead, with appropriate error handling.
+ *
+ * @param name      entry name (UTF-8 string)
+ * @param value     new entry value
+ */
+WPI_DEPRECATED("use NT_Entry function instead")
+void SetEntryTypeValue(StringRef name, std::shared_ptr<Value> value);
+
+/**
+ * Set Entry Type and Value.
+ *
+ * Sets new entry value.  If type of new value differs from the type of the
+ * currently stored entry, the currently stored entry type is overridden
+ * (generally this will generate an Entry Assignment message).
+ *
+ * This is NOT the preferred method to update a value; generally
+ * SetEntryValue() should be used instead, with appropriate error handling.
+ *
+ * @param entry     entry handle
+ * @param value     new entry value
+ */
+void SetEntryTypeValue(NT_Entry entry, std::shared_ptr<Value> value);
+
+/**
+ * Set Entry Flags.
+ *
+ * @param name      entry name (UTF-8 string)
+ * @param flags     flags value (bitmask of NT_EntryFlags)
+ */
+WPI_DEPRECATED("use NT_Entry function instead")
+void SetEntryFlags(StringRef name, unsigned int flags);
+
+/**
+ * Set Entry Flags.
+ *
+ * @param entry     entry handle
+ * @param flags     flags value (bitmask of NT_EntryFlags)
+ */
+void SetEntryFlags(NT_Entry entry, unsigned int flags);
+
+/**
+ * Get Entry Flags.
+ *
+ * @param name      entry name (UTF-8 string)
+ * @return Flags value (bitmask of NT_EntryFlags)
+ */
+WPI_DEPRECATED("use NT_Entry function instead")
+unsigned int GetEntryFlags(StringRef name);
+
+/**
+ * Get Entry Flags.
+ *
+ * @param entry     entry handle
+ * @return Flags value (bitmask of NT_EntryFlags)
+ */
+unsigned int GetEntryFlags(NT_Entry entry);
+
+/**
+ * Delete Entry.
+ *
+ * Deletes an entry.  This is a new feature in version 3.0 of the protocol,
+ * so this may not have an effect if any other node in the network is not
+ * version 3.0 or newer.
+ *
+ * Note: GetConnections() can be used to determine the protocol version
+ * of direct remote connection(s), but this is not sufficient to determine
+ * if all nodes in the network are version 3.0 or newer.
+ *
+ * @param name      entry name (UTF-8 string)
+ */
+WPI_DEPRECATED("use NT_Entry function instead")
+void DeleteEntry(StringRef name);
+
+/**
+ * Delete Entry.
+ *
+ * Deletes an entry.  This is a new feature in version 3.0 of the protocol,
+ * so this may not have an effect if any other node in the network is not
+ * version 3.0 or newer.
+ *
+ * Note: GetConnections() can be used to determine the protocol version
+ * of direct remote connection(s), but this is not sufficient to determine
+ * if all nodes in the network are version 3.0 or newer.
+ *
+ * @param entry     entry handle
+ */
+void DeleteEntry(NT_Entry entry);
+
+/**
+ * Delete All Entries.
+ *
+ * Deletes ALL table entries.  This is a new feature in version 3.0 of the
+ * so this may not have an effect if any other node in the network is not
+ * version 3.0 or newer.
+ *
+ * Note: GetConnections() can be used to determine the protocol version
+ * of direct remote connection(s), but this is not sufficient to determine
+ * if all nodes in the network are version 3.0 or newer.
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void DeleteAllEntries();
+
+/**
+ * @copydoc DeleteAllEntries()
+ *
+ * @param inst      instance handle
+ */
+void DeleteAllEntries(NT_Inst inst);
+
+/**
+ * Get Entry Information.
+ *
+ * Returns an array of entry information (name, entry type,
+ * and timestamp of last change to type/value).  The results are optionally
+ * filtered by string prefix and entry type to only return a subset of all
+ * entries.
+ *
+ * @param prefix        entry name required prefix; only entries whose name
+ *                      starts with this string are returned
+ * @param types         bitmask of NT_Type values; 0 is treated specially
+ *                      as a "don't care"
+ * @return Array of entry information.
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+std::vector<EntryInfo> GetEntryInfo(StringRef prefix, unsigned int types);
+
+/**
+ * @copydoc GetEntryInfo(StringRef, unsigned int)
+ *
+ * @param inst    instance handle
+ */
+std::vector<EntryInfo> GetEntryInfo(NT_Inst inst, const Twine& prefix,
+                                    unsigned int types);
+
+/**
+ * Get Entry Information.
+ *
+ * Returns information about an entry (name, entry type,
+ * and timestamp of last change to type/value).
+ *
+ * @param entry         entry handle
+ * @return Entry information.
+ */
+EntryInfo GetEntryInfo(NT_Entry entry);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_entrylistener_func Entry Listener Functions
+ * @{
+ */
+
+/**
+ * Entry listener callback function.
+ * Called when a key-value pair is changed.
+ *
+ * @param entry_listener  entry listener handle returned by callback creation
+ *                        function
+ * @param name            entry name
+ * @param value           the new value
+ * @param flags           update flags; for example, NT_NOTIFY_NEW if the key
+ *                        did not previously exist
+ */
+typedef std::function<void(NT_EntryListener entry_listener, StringRef name,
+                           std::shared_ptr<Value> value, unsigned int flags)>
+    EntryListenerCallback;
+
+/**
+ * Add a listener for all entries starting with a certain prefix.
+ *
+ * @param prefix            UTF-8 string prefix
+ * @param callback          listener to add
+ * @param flags             NotifyKind bitmask
+ * @return Listener handle
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+NT_EntryListener AddEntryListener(StringRef prefix,
+                                  EntryListenerCallback callback,
+                                  unsigned int flags);
+
+/**
+ * @copydoc AddEntryListener(StringRef, EntryListenerCallback, unsigned int)
+ *
+ * @param inst              instance handle
+ */
+NT_EntryListener AddEntryListener(
+    NT_Inst inst, const Twine& prefix,
+    std::function<void(const EntryNotification& event)> callback,
+    unsigned int flags);
+
+/**
+ * Add a listener for a single entry.
+ *
+ * @param entry             entry handle
+ * @param callback          listener to add
+ * @param flags             NotifyKind bitmask
+ * @return Listener handle
+ */
+NT_EntryListener AddEntryListener(
+    NT_Entry entry,
+    std::function<void(const EntryNotification& event)> callback,
+    unsigned int flags);
+
+/**
+ * Create a entry listener poller.
+ *
+ * A poller provides a single queue of poll events.  Events linked to this
+ * poller (using AddPolledEntryListener()) will be stored in the queue and
+ * must be collected by calling PollEntryListener().
+ * The returned handle must be destroyed with DestroyEntryListenerPoller().
+ *
+ * @param inst      instance handle
+ * @return poller handle
+ */
+NT_EntryListenerPoller CreateEntryListenerPoller(NT_Inst inst);
+
+/**
+ * Destroy a entry listener poller.  This will abort any blocked polling
+ * call and prevent additional events from being generated for this poller.
+ *
+ * @param poller    poller handle
+ */
+void DestroyEntryListenerPoller(NT_EntryListenerPoller poller);
+
+/**
+ * Create a polled entry listener.
+ * The caller is responsible for calling PollEntryListener() to poll.
+ *
+ * @param poller            poller handle
+ * @param prefix            UTF-8 string prefix
+ * @param flags             NotifyKind bitmask
+ * @return Listener handle
+ */
+NT_EntryListener AddPolledEntryListener(NT_EntryListenerPoller poller,
+                                        const Twine& prefix,
+                                        unsigned int flags);
+
+/**
+ * Create a polled entry listener.
+ * The caller is responsible for calling PollEntryListener() to poll.
+ *
+ * @param poller            poller handle
+ * @param prefix            UTF-8 string prefix
+ * @param flags             NotifyKind bitmask
+ * @return Listener handle
+ */
+NT_EntryListener AddPolledEntryListener(NT_EntryListenerPoller poller,
+                                        NT_Entry entry, unsigned int flags);
+
+/**
+ * Get the next entry listener event.  This blocks until the next event occurs.
+ * This is intended to be used with AddPolledEntryListener(); entry listeners
+ * created using AddEntryListener() will not be serviced through this function.
+ *
+ * @param poller    poller handle
+ * @return Information on the entry listener events.  Only returns empty if an
+ *         error occurred (e.g. the instance was invalid or is shutting down).
+ */
+std::vector<EntryNotification> PollEntryListener(NT_EntryListenerPoller poller);
+
+/**
+ * Get the next entry listener event.  This blocks until the next event occurs
+ * or it times out.  This is intended to be used with AddPolledEntryListener();
+ * entry listeners created using AddEntryListener() will not be serviced
+ * through this function.
+ *
+ * @param poller      poller handle
+ * @param timeout     timeout, in seconds
+ * @param timed_out   true if the timeout period elapsed (output)
+ * @return Information on the entry listener events.  If empty is returned and
+ *         and timed_out is also false, an error occurred (e.g. the instance
+ *         was invalid or is shutting down).
+ */
+std::vector<EntryNotification> PollEntryListener(NT_EntryListenerPoller poller,
+                                                 double timeout,
+                                                 bool* timed_out);
+
+/**
+ * Cancel a PollEntryListener call.  This wakes up a call to
+ * PollEntryListener for this poller and causes it to immediately return
+ * an empty array.
+ *
+ * @param poller  poller handle
+ */
+void CancelPollEntryListener(NT_EntryListenerPoller poller);
+
+/**
+ * Remove an entry listener.
+ *
+ * @param entry_listener Listener handle to remove
+ */
+void RemoveEntryListener(NT_EntryListener entry_listener);
+
+/**
+ * Wait for the entry listener queue to be empty.  This is primarily useful
+ * for deterministic testing.  This blocks until either the entry listener
+ * queue is empty (e.g. there are no more events that need to be passed along
+ * to callbacks or poll queues) or the timeout expires.
+ *
+ * @param inst      instance handle
+ * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+ *                  or a negative value to block indefinitely
+ * @return False if timed out, otherwise true.
+ */
+bool WaitForEntryListenerQueue(NT_Inst inst, double timeout);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_connectionlistener_func Connection Listener Functions
+ * @{
+ */
+
+/**
+ * Connection listener callback function.
+ * Called when a network connection is made or lost.
+ *
+ * @param conn_listener   connection listener handle returned by callback
+ *                        creation function
+ * @param connected       true if event is due to connection being established
+ * @param conn            connection info
+ */
+typedef std::function<void(NT_ConnectionListener conn_listener, bool connected,
+                           const ConnectionInfo& conn)>
+    ConnectionListenerCallback;
+
+/**
+ * Add a connection listener.
+ *
+ * @param callback          listener to add
+ * @param immediate_notify  notify listener of all existing connections
+ * @return Listener handle
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+NT_ConnectionListener AddConnectionListener(ConnectionListenerCallback callback,
+                                            bool immediate_notify);
+
+/**
+ * @copydoc AddConnectionListener(ConnectionListenerCallback, bool)
+ *
+ * @param inst              instance handle
+ */
+NT_ConnectionListener AddConnectionListener(
+    NT_Inst inst,
+    std::function<void(const ConnectionNotification& event)> callback,
+    bool immediate_notify);
+
+/**
+ * Create a connection listener poller.
+ *
+ * A poller provides a single queue of poll events.  Events linked to this
+ * poller (using AddPolledConnectionListener()) will be stored in the queue and
+ * must be collected by calling PollConnectionListener().
+ * The returned handle must be destroyed with DestroyConnectionListenerPoller().
+ *
+ * @param inst      instance handle
+ * @return poller handle
+ */
+NT_ConnectionListenerPoller CreateConnectionListenerPoller(NT_Inst inst);
+
+/**
+ * Destroy a connection listener poller.  This will abort any blocked polling
+ * call and prevent additional events from being generated for this poller.
+ *
+ * @param poller    poller handle
+ */
+void DestroyConnectionListenerPoller(NT_ConnectionListenerPoller poller);
+
+/**
+ * Create a polled connection listener.
+ * The caller is responsible for calling PollConnectionListener() to poll.
+ *
+ * @param poller            poller handle
+ * @param immediate_notify  notify listener of all existing connections
+ */
+NT_ConnectionListener AddPolledConnectionListener(
+    NT_ConnectionListenerPoller poller, bool immediate_notify);
+
+/**
+ * Get the next connection event.  This blocks until the next connect or
+ * disconnect occurs.  This is intended to be used with
+ * AddPolledConnectionListener(); connection listeners created using
+ * AddConnectionListener() will not be serviced through this function.
+ *
+ * @param poller    poller handle
+ * @return Information on the connection events.  Only returns empty if an
+ *         error occurred (e.g. the instance was invalid or is shutting down).
+ */
+std::vector<ConnectionNotification> PollConnectionListener(
+    NT_ConnectionListenerPoller poller);
+
+/**
+ * Get the next connection event.  This blocks until the next connect or
+ * disconnect occurs or it times out.  This is intended to be used with
+ * AddPolledConnectionListener(); connection listeners created using
+ * AddConnectionListener() will not be serviced through this function.
+ *
+ * @param poller      poller handle
+ * @param timeout     timeout, in seconds
+ * @param timed_out   true if the timeout period elapsed (output)
+ * @return Information on the connection events.  If empty is returned and
+ *         timed_out is also false, an error occurred (e.g. the instance was
+ *         invalid or is shutting down).
+ */
+std::vector<ConnectionNotification> PollConnectionListener(
+    NT_ConnectionListenerPoller poller, double timeout, bool* timed_out);
+
+/**
+ * Cancel a PollConnectionListener call.  This wakes up a call to
+ * PollConnectionListener for this poller and causes it to immediately return
+ * an empty array.
+ *
+ * @param poller  poller handle
+ */
+void CancelPollConnectionListener(NT_ConnectionListenerPoller poller);
+
+/**
+ * Remove a connection listener.
+ *
+ * @param conn_listener Listener handle to remove
+ */
+void RemoveConnectionListener(NT_ConnectionListener conn_listener);
+
+/**
+ * Wait for the connection listener queue to be empty.  This is primarily useful
+ * for deterministic testing.  This blocks until either the connection listener
+ * queue is empty (e.g. there are no more events that need to be passed along
+ * to callbacks or poll queues) or the timeout expires.
+ *
+ * @param inst      instance handle
+ * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+ *                  or a negative value to block indefinitely
+ * @return False if timed out, otherwise true.
+ */
+bool WaitForConnectionListenerQueue(NT_Inst inst, double timeout);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_rpc_func Remote Procedure Call Functions
+ * @{
+ */
+
+/**
+ * Create a callback-based RPC entry point.  Only valid to use on the server.
+ * The callback function will be called when the RPC is called.
+ *
+ * @param entry     entry handle of RPC entry
+ * @param def       RPC definition
+ * @param callback  callback function; note the callback function must call
+ *                  PostRpcResponse() to provide a response to the call
+ */
+void CreateRpc(NT_Entry entry, StringRef def,
+               std::function<void(const RpcAnswer& answer)> callback);
+
+/**
+ * Create a RPC call poller.  Only valid to use on the server.
+ *
+ * A poller provides a single queue of poll events.  Events linked to this
+ * poller (using CreatePolledRpc()) will be stored in the queue and must be
+ * collected by calling PollRpc().
+ * The returned handle must be destroyed with DestroyRpcCallPoller().
+ *
+ * @param inst      instance handle
+ * @return poller handle
+ */
+NT_RpcCallPoller CreateRpcCallPoller(NT_Inst inst);
+
+/**
+ * Destroy a RPC call poller.  This will abort any blocked polling call and
+ * prevent additional events from being generated for this poller.
+ *
+ * @param poller    poller handle
+ */
+void DestroyRpcCallPoller(NT_RpcCallPoller poller);
+
+/**
+ * Create a polled RPC entry point.  Only valid to use on the server.
+ * The caller is responsible for calling PollRpc() to poll for servicing
+ * incoming RPC calls.
+ *
+ * @param entry     entry handle of RPC entry
+ * @param def       RPC definition
+ * @param poller    poller handle
+ */
+void CreatePolledRpc(NT_Entry entry, StringRef def, NT_RpcCallPoller poller);
+
+/**
+ * Get the next incoming RPC call.  This blocks until the next incoming RPC
+ * call is received.  This is intended to be used with CreatePolledRpc();
+ * RPC calls created using CreateRpc() will not be serviced through this
+ * function.  Upon successful return, PostRpcResponse() must be called to
+ * send the return value to the caller.
+ *
+ * @param poller      poller handle
+ * @return Information on the next RPC calls.  Only returns empty if an error
+ *         occurred (e.g. the instance was invalid or is shutting down).
+ */
+std::vector<RpcAnswer> PollRpc(NT_RpcCallPoller poller);
+
+/**
+ * Get the next incoming RPC call.  This blocks until the next incoming RPC
+ * call is received or it times out.  This is intended to be used with
+ * CreatePolledRpc(); RPC calls created using CreateRpc() will not be
+ * serviced through this function.  Upon successful return,
+ * PostRpcResponse() must be called to send the return value to the caller.
+ *
+ * @param poller      poller handle
+ * @param timeout     timeout, in seconds
+ * @param timed_out   true if the timeout period elapsed (output)
+ * @return Information on the next RPC calls.  If empty and timed_out is also
+ *         false, an error occurred (e.g. the instance was invalid or is
+ *         shutting down).
+ */
+std::vector<RpcAnswer> PollRpc(NT_RpcCallPoller poller, double timeout,
+                               bool* timed_out);
+
+/**
+ * Cancel a PollRpc call.  This wakes up a call to PollRpc for this poller
+ * and causes it to immediately return an empty array.
+ *
+ * @param poller  poller handle
+ */
+void CancelPollRpc(NT_RpcCallPoller poller);
+
+/**
+ * Wait for the incoming RPC call queue to be empty.  This is primarily useful
+ * for deterministic testing.  This blocks until either the RPC call
+ * queue is empty (e.g. there are no more events that need to be passed along
+ * to callbacks or poll queues) or the timeout expires.
+ *
+ * @param inst      instance handle
+ * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+ *                  or a negative value to block indefinitely
+ * @return False if timed out, otherwise true.
+ */
+bool WaitForRpcCallQueue(NT_Inst inst, double timeout);
+
+/**
+ * Post RPC response (return value) for a polled RPC.
+ * The rpc and call parameters should come from the RpcAnswer returned
+ * by PollRpc().
+ *
+ * @param entry       entry handle of RPC entry (from RpcAnswer)
+ * @param call        RPC call handle (from RpcAnswer)
+ * @param result      result raw data that will be provided to remote caller
+ * @return            true if the response was posted, otherwise false
+ */
+bool PostRpcResponse(NT_Entry entry, NT_RpcCall call, StringRef result);
+
+/**
+ * Call a RPC function.  May be used on either the client or server.
+ * This function is non-blocking.  Either GetRpcResult() or
+ * CancelRpcResult() must be called to either get or ignore the result of
+ * the call.
+ *
+ * @param entry       entry handle of RPC entry
+ * @param params      parameter
+ * @return RPC call handle (for use with GetRpcResult() or
+ *         CancelRpcResult()).
+ */
+NT_RpcCall CallRpc(NT_Entry entry, StringRef params);
+
+/**
+ * Get the result (return value) of a RPC call.  This function blocks until
+ * the result is received.
+ *
+ * @param entry       entry handle of RPC entry
+ * @param call        RPC call handle returned by CallRpc()
+ * @param result      received result (output)
+ * @return False on error, true otherwise.
+ */
+bool GetRpcResult(NT_Entry entry, NT_RpcCall call, std::string* result);
+
+/**
+ * Get the result (return value) of a RPC call.  This function blocks until
+ * the result is received or it times out.
+ *
+ * @param entry       entry handle of RPC entry
+ * @param call        RPC call handle returned by CallRpc()
+ * @param result      received result (output)
+ * @param timeout     timeout, in seconds
+ * @param timed_out   true if the timeout period elapsed (output)
+ * @return False on error or timeout, true otherwise.
+ */
+bool GetRpcResult(NT_Entry entry, NT_RpcCall call, std::string* result,
+                  double timeout, bool* timed_out);
+
+/**
+ * Ignore the result of a RPC call.  This function is non-blocking.
+ *
+ * @param entry       entry handle of RPC entry
+ * @param call        RPC call handle returned by CallRpc()
+ */
+void CancelRpcResult(NT_Entry entry, NT_RpcCall call);
+
+/**
+ * Pack a RPC version 1 definition.
+ *
+ * @param def         RPC version 1 definition
+ * @return Raw packed bytes.
+ */
+std::string PackRpcDefinition(const RpcDefinition& def);
+
+/**
+ * Unpack a RPC version 1 definition.  This can be used for introspection or
+ * validation.
+ *
+ * @param packed      raw packed bytes
+ * @param def         RPC version 1 definition (output)
+ * @return True if successfully unpacked, false otherwise.
+ */
+bool UnpackRpcDefinition(StringRef packed, RpcDefinition* def);
+
+/**
+ * Pack RPC values as required for RPC version 1 definition messages.
+ *
+ * @param values      array of values to pack
+ * @return Raw packed bytes.
+ */
+std::string PackRpcValues(ArrayRef<std::shared_ptr<Value>> values);
+
+/**
+ * Unpack RPC values as required for RPC version 1 definition messages.
+ *
+ * @param packed      raw packed bytes
+ * @param types       array of data types (as provided in the RPC definition)
+ * @return Array of values.
+ */
+std::vector<std::shared_ptr<Value>> UnpackRpcValues(StringRef packed,
+                                                    ArrayRef<NT_Type> types);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_network_func Client/Server Functions
+ * @{
+ */
+
+/**
+ * Set the network identity of this node.
+ * This is the name used during the initial connection handshake, and is
+ * visible through ConnectionInfo on the remote node.
+ *
+ * @param name      identity to advertise
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void SetNetworkIdentity(StringRef name);
+
+/**
+ * @copydoc SetNetworkIdentity(StringRef)
+ *
+ * @param inst      instance handle
+ */
+void SetNetworkIdentity(NT_Inst inst, const Twine& name);
+
+/**
+ * Get the current network mode.
+ *
+ * @return Bitmask of NT_NetworkMode.
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+unsigned int GetNetworkMode();
+
+/**
+ * Get the current network mode.
+ *
+ * @param inst  instance handle
+ * @return Bitmask of NT_NetworkMode.
+ */
+unsigned int GetNetworkMode(NT_Inst inst);
+
+/**
+ * Starts a server using the specified filename, listening address, and port.
+ *
+ * @param persist_filename  the name of the persist file to use (UTF-8 string,
+ *                          null terminated)
+ * @param listen_address    the address to listen on, or null to listen on any
+ *                          address. (UTF-8 string, null terminated)
+ * @param port              port to communicate over.
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void StartServer(StringRef persist_filename, const char* listen_address,
+                 unsigned int port);
+
+/**
+ * @copydoc StartServer(StringRef, const char*, unsigned int)
+ *
+ * @param inst              instance handle
+ */
+void StartServer(NT_Inst inst, const Twine& persist_filename,
+                 const char* listen_address, unsigned int port);
+
+/**
+ * Stops the server if it is running.
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void StopServer();
+
+/**
+ * @copydoc StopServer()
+ *
+ * @param inst  instance handle
+ */
+void StopServer(NT_Inst inst);
+
+/**
+ * Starts a client.  Use SetServer to set the server name and port.
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void StartClient();
+
+/**
+ * Starts a client using the specified server and port
+ *
+ * @param server_name server name (UTF-8 string, null terminated)
+ * @param port        port to communicate over
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void StartClient(const char* server_name, unsigned int port);
+
+/**
+ * Starts a client using the specified (server, port) combinations.  The
+ * client will attempt to connect to each server in round robin fashion.
+ *
+ * @param servers   array of server name and port pairs
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void StartClient(ArrayRef<std::pair<StringRef, unsigned int>> servers);
+
+/**
+ * @copydoc StartClient()
+ *
+ * @param inst  instance handle
+ */
+void StartClient(NT_Inst inst);
+
+/**
+ * @copydoc StartClient(const char*, unsigned int)
+ *
+ * @param inst        instance handle
+ */
+void StartClient(NT_Inst inst, const char* server_name, unsigned int port);
+
+/**
+ * @copydoc StartClient(ArrayRef<std::pair<StringRef, unsigned int>>)
+ *
+ * @param inst      instance handle
+ */
+void StartClient(NT_Inst inst,
+                 ArrayRef<std::pair<StringRef, unsigned int>> servers);
+
+/**
+ * Starts a client using commonly known robot addresses for the specified
+ * team.
+ *
+ * @param inst        instance handle
+ * @param team        team number
+ * @param port        port to communicate over
+ */
+void StartClientTeam(NT_Inst inst, unsigned int team, unsigned int port);
+
+/**
+ * Stops the client if it is running.
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void StopClient();
+
+/**
+ * @copydoc StopClient()
+ * @param inst  instance handle
+ */
+void StopClient(NT_Inst inst);
+
+/**
+ * Sets server address and port for client (without restarting client).
+ *
+ * @param server_name server name (UTF-8 string, null terminated)
+ * @param port        port to communicate over
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void SetServer(const char* server_name, unsigned int port);
+
+/**
+ * Sets server addresses for client (without restarting client).
+ * The client will attempt to connect to each server in round robin fashion.
+ *
+ * @param servers   array of server name and port pairs
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void SetServer(ArrayRef<std::pair<StringRef, unsigned int>> servers);
+
+/**
+ * @copydoc SetServer(const char*, unsigned int)
+ *
+ * @param inst        instance handle
+ */
+void SetServer(NT_Inst inst, const char* server_name, unsigned int port);
+
+/**
+ * @copydoc SetServer(ArrayRef<std::pair<StringRef, unsigned int>>)
+ *
+ * @param inst      instance handle
+ */
+void SetServer(NT_Inst inst,
+               ArrayRef<std::pair<StringRef, unsigned int>> servers);
+
+/**
+ * Sets server addresses and port for client (without restarting client).
+ * Connects using commonly known robot addresses for the specified team.
+ *
+ * @param inst        instance handle
+ * @param team        team number
+ * @param port        port to communicate over
+ */
+void SetServerTeam(NT_Inst inst, unsigned int team, unsigned int port);
+
+/**
+ * Starts requesting server address from Driver Station.
+ * This connects to the Driver Station running on localhost to obtain the
+ * server IP address.
+ *
+ * @param port server port to use in combination with IP from DS
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void StartDSClient(unsigned int port);
+
+/**
+ * @copydoc StartDSClient(unsigned int)
+ * @param inst  instance handle
+ */
+void StartDSClient(NT_Inst inst, unsigned int port);
+
+/** Stops requesting server address from Driver Station. */
+WPI_DEPRECATED("use NT_Inst function instead")
+void StopDSClient();
+
+/**
+ * @copydoc StopDSClient()
+ *
+ * @param inst  instance handle
+ */
+void StopDSClient(NT_Inst inst);
+
+/** Stops the RPC server if it is running. */
+WPI_DEPRECATED("use NT_Inst function instead")
+void StopRpcServer();
+
+/**
+ * Set the periodic update rate.
+ * Sets how frequently updates are sent to other nodes over the network.
+ *
+ * @param interval  update interval in seconds (range 0.01 to 1.0)
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void SetUpdateRate(double interval);
+
+/**
+ * @copydoc SetUpdateRate(double)
+ *
+ * @param inst      instance handle
+ */
+void SetUpdateRate(NT_Inst inst, double interval);
+
+/**
+ * Flush Entries.
+ *
+ * Forces an immediate flush of all local entry changes to network.
+ * Normally this is done on a regularly scheduled interval (see
+ * SetUpdateRate()).
+ *
+ * Note: flushes are rate limited to avoid excessive network traffic.  If
+ * the time between calls is too short, the flush will occur after the minimum
+ * time elapses (rather than immediately).
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void Flush();
+
+/**
+ * @copydoc Flush()
+ *
+ * @param inst      instance handle
+ */
+void Flush(NT_Inst inst);
+
+/**
+ * Get information on the currently established network connections.
+ * If operating as a client, this will return either zero or one values.
+ *
+ * @return      array of connection information
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+std::vector<ConnectionInfo> GetConnections();
+
+/**
+ * @copydoc GetConnections()
+ *
+ * @param inst  instance handle
+ */
+std::vector<ConnectionInfo> GetConnections(NT_Inst inst);
+
+/**
+ * Return whether or not the instance is connected to another node.
+ *
+ * @param inst  instance handle
+ * @return True if connected.
+ */
+bool IsConnected(NT_Inst inst);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_file_func File Save/Load Functions
+ * @{
+ */
+
+/**
+ * Save persistent values to a file.  The server automatically does this,
+ * but this function provides a way to save persistent values in the same
+ * format to a file on either a client or a server.
+ *
+ * @param filename  filename
+ * @return error string, or nullptr if successful
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+const char* SavePersistent(StringRef filename);
+
+/**
+ * @copydoc SavePersistent(StringRef)
+ * @param inst      instance handle
+ */
+const char* SavePersistent(NT_Inst inst, const Twine& filename);
+
+/**
+ * Load persistent values from a file.  The server automatically does this
+ * at startup, but this function provides a way to restore persistent values
+ * in the same format from a file at any time on either a client or a server.
+ *
+ * @param filename  filename
+ * @param warn      callback function for warnings
+ * @return error string, or nullptr if successful
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+const char* LoadPersistent(
+    StringRef filename, std::function<void(size_t line, const char* msg)> warn);
+
+/**
+ * @copydoc LoadPersistent(StringRef, std::function<void(size_t, const
+ * char*)>)
+ *
+ * @param inst      instance handle
+ */
+const char* LoadPersistent(
+    NT_Inst inst, const Twine& filename,
+    std::function<void(size_t line, const char* msg)> warn);
+
+/**
+ * Save table values to a file.  The file format used is identical to
+ * that used for SavePersistent.
+ *
+ * @param inst      instance handle
+ * @param filename  filename
+ * @param prefix    save only keys starting with this prefix
+ * @return error string, or nullptr if successful
+ */
+const char* SaveEntries(NT_Inst inst, const Twine& filename,
+                        const Twine& prefix);
+
+/**
+ * Load table values from a file.  The file format used is identical to
+ * that used for SavePersistent / LoadPersistent.
+ *
+ * @param inst      instance handle
+ * @param filename  filename
+ * @param prefix    load only keys starting with this prefix
+ * @param warn      callback function for warnings
+ * @return error string, or nullptr if successful
+ */
+const char* LoadEntries(NT_Inst inst, const Twine& filename,
+                        const Twine& prefix,
+                        std::function<void(size_t line, const char* msg)> warn);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_utility_func Utility Functions
+ * @{
+ */
+
+/**
+ * Returns monotonic current time in 1 us increments.
+ * This is the same time base used for entry and connection timestamps.
+ * This function is a compatibility wrapper around wpi::Now().
+ *
+ * @return Timestamp
+ */
+uint64_t Now();
+
+/** @} */
+
+/**
+ * @defgroup ntcore_logger_func Logger Functions
+ * @{
+ */
+
+/**
+ * Log function.
+ *
+ * @param level   log level of the message (see NT_LogLevel)
+ * @param file    origin source filename
+ * @param line    origin source line number
+ * @param msg     message
+ */
+typedef std::function<void(unsigned int level, const char* file,
+                           unsigned int line, const char* msg)>
+    LogFunc;
+
+/**
+ * Set logger callback function.  By default, log messages are sent to stderr;
+ * this function changes the log level and sends log messages to the provided
+ * callback function instead.  The callback function will only be called for
+ * log messages with level greater than or equal to min_level; messages lower
+ * than this level will be silently ignored.
+ *
+ * @param func        log callback function
+ * @param min_level   minimum log level
+ */
+WPI_DEPRECATED("use NT_Inst function instead")
+void SetLogger(LogFunc func, unsigned int min_level);
+
+/**
+ * Add logger callback function.  By default, log messages are sent to stderr;
+ * this function sends log messages to the provided callback function instead.
+ * The callback function will only be called for log messages with level
+ * greater than or equal to min_level and less than or equal to max_level;
+ * messages outside this range will be silently ignored.
+ *
+ * @param inst        instance handle
+ * @param func        log callback function
+ * @param min_level   minimum log level
+ * @param max_level   maximum log level
+ * @return Logger handle
+ */
+NT_Logger AddLogger(NT_Inst inst,
+                    std::function<void(const LogMessage& msg)> func,
+                    unsigned int min_level, unsigned int max_level);
+
+/**
+ * Create a log poller.  A poller provides a single queue of poll events.
+ * The returned handle must be destroyed with DestroyLoggerPoller().
+ *
+ * @param inst      instance handle
+ * @return poller handle
+ */
+NT_LoggerPoller CreateLoggerPoller(NT_Inst inst);
+
+/**
+ * Destroy a log poller.  This will abort any blocked polling call and prevent
+ * additional events from being generated for this poller.
+ *
+ * @param poller    poller handle
+ */
+void DestroyLoggerPoller(NT_LoggerPoller poller);
+
+/**
+ * Set the log level for a log poller.  Events will only be generated for
+ * log messages with level greater than or equal to min_level and less than or
+ * equal to max_level; messages outside this range will be silently ignored.
+ *
+ * @param poller        poller handle
+ * @param min_level     minimum log level
+ * @param max_level     maximum log level
+ * @return Logger handle
+ */
+NT_Logger AddPolledLogger(NT_LoggerPoller poller, unsigned int min_level,
+                          unsigned int max_level);
+
+/**
+ * Get the next log event.  This blocks until the next log occurs.
+ *
+ * @param poller    poller handle
+ * @return Information on the log events.  Only returns empty if an error
+ *         occurred (e.g. the instance was invalid or is shutting down).
+ */
+std::vector<LogMessage> PollLogger(NT_LoggerPoller poller);
+
+/**
+ * Get the next log event.  This blocks until the next log occurs or it times
+ * out.
+ *
+ * @param poller      poller handle
+ * @param timeout     timeout, in seconds
+ * @param timed_out   true if the timeout period elapsed (output)
+ * @return Information on the log events.  If empty is returned and timed_out
+ *         is also false, an error occurred (e.g. the instance was invalid or
+ *         is shutting down).
+ */
+std::vector<LogMessage> PollLogger(NT_LoggerPoller poller, double timeout,
+                                   bool* timed_out);
+
+/**
+ * Cancel a PollLogger call.  This wakes up a call to PollLogger for this
+ * poller and causes it to immediately return an empty array.
+ *
+ * @param poller  poller handle
+ */
+void CancelPollLogger(NT_LoggerPoller poller);
+
+/**
+ * Remove a logger.
+ *
+ * @param logger Logger handle to remove
+ */
+void RemoveLogger(NT_Logger logger);
+
+/**
+ * Wait for the incoming log event queue to be empty.  This is primarily useful
+ * for deterministic testing.  This blocks until either the log event
+ * queue is empty (e.g. there are no more events that need to be passed along
+ * to callbacks or poll queues) or the timeout expires.
+ *
+ * @param inst      instance handle
+ * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
+ *                  or a negative value to block indefinitely
+ * @return False if timed out, otherwise true.
+ */
+bool WaitForLoggerQueue(NT_Inst inst, double timeout);
+
+/** @} */
+/** @} */
+
+inline bool RpcAnswer::PostResponse(StringRef result) const {
+  auto ret = PostRpcResponse(entry, call, result);
+  call = 0;
+  return ret;
+}
+
+}  // namespace nt
+
+#endif  // NTCORE_NTCORE_CPP_H_
diff --git a/ntcore/src/main/native/include/ntcore_test.h b/ntcore/src/main/native/include/ntcore_test.h
new file mode 100644
index 0000000..920fd68
--- /dev/null
+++ b/ntcore/src/main/native/include/ntcore_test.h
@@ -0,0 +1,89 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2016-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_NTCORE_TEST_H_
+#define NTCORE_NTCORE_TEST_H_
+
+#include <stdint.h>
+
+#include <string>
+
+#include "ntcore.h"
+
+// Functions in this header are to be used only for testing
+
+extern "C" {
+struct NT_String* NT_GetStringForTesting(const char* string, int* struct_size);
+// No need for free as one already exists in main library
+
+struct NT_EntryInfo* NT_GetEntryInfoForTesting(const char* name,
+                                               enum NT_Type type,
+                                               unsigned int flags,
+                                               uint64_t last_change,
+                                               int* struct_size);
+
+void NT_FreeEntryInfoForTesting(struct NT_EntryInfo* info);
+
+struct NT_ConnectionInfo* NT_GetConnectionInfoForTesting(
+    const char* remote_id, const char* remote_ip, unsigned int remote_port,
+    uint64_t last_update, unsigned int protocol_version, int* struct_size);
+
+void NT_FreeConnectionInfoForTesting(struct NT_ConnectionInfo* info);
+
+struct NT_Value* NT_GetValueBooleanForTesting(uint64_t last_change, int val,
+                                              int* struct_size);
+
+struct NT_Value* NT_GetValueDoubleForTesting(uint64_t last_change, double val,
+                                             int* struct_size);
+
+struct NT_Value* NT_GetValueStringForTesting(uint64_t last_change,
+                                             const char* str, int* struct_size);
+
+struct NT_Value* NT_GetValueRawForTesting(uint64_t last_change, const char* raw,
+                                          int raw_len, int* struct_size);
+
+struct NT_Value* NT_GetValueBooleanArrayForTesting(uint64_t last_change,
+                                                   const int* arr,
+                                                   size_t array_len,
+                                                   int* struct_size);
+
+struct NT_Value* NT_GetValueDoubleArrayForTesting(uint64_t last_change,
+                                                  const double* arr,
+                                                  size_t array_len,
+                                                  int* struct_size);
+
+struct NT_Value* NT_GetValueStringArrayForTesting(uint64_t last_change,
+                                                  const struct NT_String* arr,
+                                                  size_t array_len,
+                                                  int* struct_size);
+// No need for free as one already exists in the main library
+
+struct NT_RpcParamDef* NT_GetRpcParamDefForTesting(const char* name,
+                                                   const struct NT_Value* val,
+                                                   int* struct_size);
+
+void NT_FreeRpcParamDefForTesting(struct NT_RpcParamDef* def);
+
+struct NT_RpcResultDef* NT_GetRpcResultsDefForTesting(const char* name,
+                                                      enum NT_Type type,
+                                                      int* struct_size);
+
+void NT_FreeRpcResultsDefForTesting(struct NT_RpcResultDef* def);
+
+struct NT_RpcDefinition* NT_GetRpcDefinitionForTesting(
+    unsigned int version, const char* name, size_t num_params,
+    const struct NT_RpcParamDef* params, size_t num_results,
+    const struct NT_RpcResultDef* results, int* struct_size);
+// No need for free as one already exists in the main library
+
+struct NT_RpcCallInfo* NT_GetRpcCallInfoForTesting(
+    unsigned int rpc_id, unsigned int call_uid, const char* name,
+    const char* params, size_t params_len, int* struct_size);
+// No need for free as one already exists in the main library
+}  // extern "C"
+
+#endif  // NTCORE_NTCORE_TEST_H_
diff --git a/ntcore/src/main/native/include/tables/ITable.h b/ntcore/src/main/native/include/tables/ITable.h
new file mode 100644
index 0000000..d03aaa7
--- /dev/null
+++ b/ntcore/src/main/native/include/tables/ITable.h
@@ -0,0 +1,456 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_TABLES_ITABLE_H_
+#define NTCORE_TABLES_ITABLE_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <wpi/StringRef.h>
+#include <wpi/Twine.h>
+#include <wpi/deprecated.h>
+
+#include "networktables/NetworkTableValue.h"
+
+namespace nt {
+class NetworkTable;
+}  // namespace nt
+
+class ITableListener;
+
+/**
+ * A table whose values can be read and written to
+ */
+class WPI_DEPRECATED("Use NetworkTable directly") ITable {
+ public:
+  /**
+   * Determines whether the given key is in this table.
+   *
+   * @param key the key to search for
+   * @return true if the table as a value assigned to the given key
+   */
+  virtual bool ContainsKey(const wpi::Twine& key) const = 0;
+
+  /**
+   * Determines whether there exists a non-empty subtable for this key
+   * in this table.
+   *
+   * @param key the key to search for
+   * @return true if there is a subtable with the key which contains at least
+   * one key/subtable of its own
+   */
+  virtual bool ContainsSubTable(const wpi::Twine& key) const = 0;
+
+  /**
+   * Gets the subtable in this table for the given name.
+   *
+   * @param key the name of the table relative to this one
+   * @return a sub table relative to this one
+   */
+  virtual std::shared_ptr<nt::NetworkTable> GetSubTable(
+      const wpi::Twine& key) const = 0;
+
+  /**
+   * @param types bitmask of types; 0 is treated as a "don't care".
+   * @return keys currently in the table
+   */
+  virtual std::vector<std::string> GetKeys(int types = 0) const = 0;
+
+  /**
+   * @return subtables currently in the table
+   */
+  virtual std::vector<std::string> GetSubTables() const = 0;
+
+  /**
+   * Makes a key's value persistent through program restarts.
+   *
+   * @param key the key to make persistent
+   */
+  virtual void SetPersistent(wpi::StringRef key) = 0;
+
+  /**
+   * Stop making a key's value persistent through program restarts.
+   * The key cannot be null.
+   *
+   * @param key the key name
+   */
+  virtual void ClearPersistent(wpi::StringRef key) = 0;
+
+  /**
+   * Returns whether the value is persistent through program restarts.
+   * The key cannot be null.
+   *
+   * @param key the key name
+   */
+  virtual bool IsPersistent(wpi::StringRef key) const = 0;
+
+  /**
+   * Sets flags on the specified key in this table. The key can
+   * not be null.
+   *
+   * @param key the key name
+   * @param flags the flags to set (bitmask)
+   */
+  virtual void SetFlags(wpi::StringRef key, unsigned int flags) = 0;
+
+  /**
+   * Clears flags on the specified key in this table. The key can
+   * not be null.
+   *
+   * @param key the key name
+   * @param flags the flags to clear (bitmask)
+   */
+  virtual void ClearFlags(wpi::StringRef key, unsigned int flags) = 0;
+
+  /**
+   * Returns the flags for the specified key.
+   *
+   * @param key the key name
+   * @return the flags, or 0 if the key is not defined
+   */
+  virtual unsigned int GetFlags(wpi::StringRef key) const = 0;
+
+  /**
+   * Deletes the specified key in this table.
+   *
+   * @param key the key name
+   */
+  virtual void Delete(const wpi::Twine& key) = 0;
+
+  /**
+   * Gets the value associated with a key as an object
+   *
+   * @param key the key of the value to look up
+   * @return the value associated with the given key, or nullptr if the key
+   * does not exist
+   */
+  virtual std::shared_ptr<nt::Value> GetValue(const wpi::Twine& key) const = 0;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  virtual bool SetDefaultValue(const wpi::Twine& key,
+                               std::shared_ptr<nt::Value> defaultValue) = 0;
+
+  /**
+   * Put a value in the table
+   *
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  virtual bool PutValue(const wpi::Twine& key,
+                        std::shared_ptr<nt::Value> value) = 0;
+
+  /**
+   * Put a number in the table
+   *
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  virtual bool PutNumber(wpi::StringRef key, double value) = 0;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  virtual bool SetDefaultNumber(wpi::StringRef key, double defaultValue) = 0;
+
+  /**
+   * Gets the number associated with the given name.
+   *
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  virtual double GetNumber(wpi::StringRef key, double defaultValue) const = 0;
+
+  /**
+   * Put a string in the table
+   *
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  virtual bool PutString(wpi::StringRef key, wpi::StringRef value) = 0;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  virtual bool SetDefaultString(wpi::StringRef key,
+                                wpi::StringRef defaultValue) = 0;
+
+  /**
+   * Gets the string associated with the given name. If the key does not
+   * exist or is of different type, it will return the default value.
+   *
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   *
+   * @note This makes a copy of the string.  If the overhead of this is a
+   *       concern, use GetValue() instead.
+   */
+  virtual std::string GetString(wpi::StringRef key,
+                                wpi::StringRef defaultValue) const = 0;
+
+  /**
+   * Put a boolean in the table
+   *
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  virtual bool PutBoolean(wpi::StringRef key, bool value) = 0;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  virtual bool SetDefaultBoolean(wpi::StringRef key, bool defaultValue) = 0;
+
+  /**
+   * Gets the boolean associated with the given name. If the key does not
+   * exist or is of different type, it will return the default value.
+   *
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   */
+  virtual bool GetBoolean(wpi::StringRef key, bool defaultValue) const = 0;
+
+  /**
+   * Put a boolean array in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   *
+   * @note The array must be of int's rather than of bool's because
+   *       std::vector<bool> is special-cased in C++.  0 is false, any
+   *       non-zero value is true.
+   */
+  virtual bool PutBooleanArray(wpi::StringRef key,
+                               wpi::ArrayRef<int> value) = 0;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  virtual bool SetDefaultBooleanArray(wpi::StringRef key,
+                                      wpi::ArrayRef<int> defaultValue) = 0;
+
+  /**
+   * Returns the boolean array the key maps to. If the key does not exist or is
+   * of different type, it will return the default value.
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   *
+   * @note This makes a copy of the array.  If the overhead of this is a
+   *       concern, use GetValue() instead.
+   *
+   * @note The returned array is std::vector<int> instead of std::vector<bool>
+   *       because std::vector<bool> is special-cased in C++.  0 is false, any
+   *       non-zero value is true.
+   */
+  virtual std::vector<int> GetBooleanArray(
+      wpi::StringRef key, wpi::ArrayRef<int> defaultValue) const = 0;
+
+  /**
+   * Put a number array in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  virtual bool PutNumberArray(wpi::StringRef key,
+                              wpi::ArrayRef<double> value) = 0;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  virtual bool SetDefaultNumberArray(wpi::StringRef key,
+                                     wpi::ArrayRef<double> defaultValue) = 0;
+
+  /**
+   * Returns the number array the key maps to. If the key does not exist or is
+   * of different type, it will return the default value.
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   *
+   * @note This makes a copy of the array.  If the overhead of this is a
+   *       concern, use GetValue() instead.
+   */
+  virtual std::vector<double> GetNumberArray(
+      wpi::StringRef key, wpi::ArrayRef<double> defaultValue) const = 0;
+
+  /**
+   * Put a string array in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  virtual bool PutStringArray(wpi::StringRef key,
+                              wpi::ArrayRef<std::string> value) = 0;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  virtual bool SetDefaultStringArray(
+      wpi::StringRef key, wpi::ArrayRef<std::string> defaultValue) = 0;
+
+  /**
+   * Returns the string array the key maps to. If the key does not exist or is
+   * of different type, it will return the default value.
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   *
+   * @note This makes a copy of the array.  If the overhead of this is a
+   *       concern, use GetValue() instead.
+   */
+  virtual std::vector<std::string> GetStringArray(
+      wpi::StringRef key, wpi::ArrayRef<std::string> defaultValue) const = 0;
+
+  /**
+   * Put a raw value (byte array) in the table
+   * @param key the key to be assigned to
+   * @param value the value that will be assigned
+   * @return False if the table key already exists with a different type
+   */
+  virtual bool PutRaw(wpi::StringRef key, wpi::StringRef value) = 0;
+
+  /**
+   * Gets the current value in the table, setting it if it does not exist.
+   * @param key the key
+   * @param defaultValue the default value to set if key doesn't exist.
+   * @returns False if the table key exists with a different type
+   */
+  virtual bool SetDefaultRaw(wpi::StringRef key,
+                             wpi::StringRef defaultValue) = 0;
+
+  /**
+   * Returns the raw value (byte array) the key maps to. If the key does not
+   * exist or is of different type, it will return the default value.
+   * @param key the key to look up
+   * @param defaultValue the value to be returned if no value is found
+   * @return the value associated with the given key or the given default value
+   * if there is no value associated with the key
+   *
+   * @note This makes a copy of the raw contents.  If the overhead of this is a
+   *       concern, use GetValue() instead.
+   */
+  virtual std::string GetRaw(wpi::StringRef key,
+                             wpi::StringRef defaultValue) const = 0;
+
+  /**
+   * Add a listener for changes to the table
+   *
+   * @param listener the listener to add
+   */
+  virtual void AddTableListener(ITableListener* listener) = 0;
+
+  /**
+   * Add a listener for changes to the table
+   *
+   * @param listener the listener to add
+   * @param immediateNotify if true then this listener will be notified of all
+   * current entries (marked as new)
+   */
+  virtual void AddTableListener(ITableListener* listener,
+                                bool immediateNotify) = 0;
+
+  /**
+   * Add a listener for changes to the table
+   *
+   * @param listener the listener to add
+   * @param immediateNotify if true then this listener will be notified of all
+   * current entries (marked as new)
+   * @param flags bitmask of NT_NotifyKind specifying desired notifications
+   */
+  virtual void AddTableListenerEx(ITableListener* listener,
+                                  unsigned int flags) = 0;
+
+  /**
+   * Add a listener for changes to a specific key the table
+   *
+   * @param key the key to listen for
+   * @param listener the listener to add
+   * @param immediateNotify if true then this listener will be notified of all
+   * current entries (marked as new)
+   */
+  virtual void AddTableListener(wpi::StringRef key, ITableListener* listener,
+                                bool immediateNotify) = 0;
+
+  /**
+   * Add a listener for changes to a specific key the table
+   *
+   * @param key the key to listen for
+   * @param listener the listener to add
+   * @param immediateNotify if true then this listener will be notified of all
+   * current entries (marked as new)
+   * @param flags bitmask of NT_NotifyKind specifying desired notifications
+   */
+  virtual void AddTableListenerEx(wpi::StringRef key, ITableListener* listener,
+                                  unsigned int flags) = 0;
+
+  /**
+   * This will immediately notify the listener of all current sub tables
+   * @param listener the listener to add
+   */
+  virtual void AddSubTableListener(ITableListener* listener) = 0;
+
+  /**
+   * This will immediately notify the listener of all current sub tables
+   * @param listener the listener to add
+   * @param localNotify if true then this listener will be notified of all
+   * local changes in addition to all remote changes
+   */
+  virtual void AddSubTableListener(ITableListener* listener,
+                                   bool localNotify) = 0;
+
+  /**
+   * Remove a listener from receiving table events
+   *
+   * @param listener the listener to be removed
+   */
+  virtual void RemoveTableListener(ITableListener* listener) = 0;
+
+  /**
+   * Gets the full path of this table.
+   */
+  virtual wpi::StringRef GetPath() const = 0;
+};
+
+#endif  // NTCORE_TABLES_ITABLE_H_
diff --git a/ntcore/src/main/native/include/tables/ITableListener.h b/ntcore/src/main/native/include/tables/ITableListener.h
new file mode 100644
index 0000000..e836b51
--- /dev/null
+++ b/ntcore/src/main/native/include/tables/ITableListener.h
@@ -0,0 +1,63 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_TABLES_ITABLELISTENER_H_
+#define NTCORE_TABLES_ITABLELISTENER_H_
+
+#include <memory>
+
+#include <wpi/StringRef.h>
+#include <wpi/deprecated.h>
+
+#include "networktables/NetworkTableValue.h"
+
+#ifdef __GNUC__
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
+class ITable;
+
+/**
+ * A listener that listens to changes in values in a {@link ITable}
+ */
+class WPI_DEPRECATED(
+    "Use EntryListener, TableEntryListener, or TableListener as appropriate")
+    ITableListener {
+ public:
+  virtual ~ITableListener() = default;
+  /**
+   * Called when a key-value pair is changed in a {@link ITable}
+   * @param source the table the key-value pair exists in
+   * @param key the key associated with the value that changed
+   * @param value the new value
+   * @param isNew true if the key did not previously exist in the table,
+   * otherwise it is false
+   */
+  virtual void ValueChanged(ITable* source, wpi::StringRef key,
+                            std::shared_ptr<nt::Value> value, bool isNew) = 0;
+
+  /**
+   * Extended version of ValueChanged.  Called when a key-value pair is
+   * changed in a {@link ITable}.  The default implementation simply calls
+   * ValueChanged().  If this is overridden, ValueChanged() will not be called.
+   * @param source the table the key-value pair exists in
+   * @param key the key associated with the value that changed
+   * @param value the new value
+   * @param flags update flags; for example, NT_NOTIFY_NEW if the key did not
+   * previously exist in the table
+   */
+  virtual void ValueChangedEx(ITable* source, wpi::StringRef key,
+                              std::shared_ptr<nt::Value> value,
+                              unsigned int flags);
+};
+
+#ifdef __GNUC__
+#pragma GCC diagnostic pop
+#endif
+
+#endif  // NTCORE_TABLES_ITABLELISTENER_H_
diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/ConnectionListenerTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/ConnectionListenerTest.java
new file mode 100644
index 0000000..9628a0e
--- /dev/null
+++ b/ntcore/src/test/java/edu/wpi/first/networktables/ConnectionListenerTest.java
@@ -0,0 +1,160 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+class ConnectionListenerTest {
+  private NetworkTableInstance m_serverInst;
+  private NetworkTableInstance m_clientInst;
+
+  @BeforeEach
+  void setUp() {
+    m_serverInst = NetworkTableInstance.create();
+    m_serverInst.setNetworkIdentity("server");
+
+    m_clientInst = NetworkTableInstance.create();
+    m_clientInst.setNetworkIdentity("client");
+  }
+
+  @AfterEach
+  void tearDown() {
+    m_clientInst.close();
+    m_serverInst.close();
+  }
+
+  /**
+   * Connect to the server.
+   */
+  @SuppressWarnings("PMD.AvoidUsingHardCodedIP")
+  private void connect() {
+    m_serverInst.startServer("connectionlistenertest.ini", "127.0.0.1", 10000);
+    m_clientInst.startClient("127.0.0.1", 10000);
+
+    // wait for client to report it's started, then wait another 0.1 sec
+    try {
+      while ((m_clientInst.getNetworkMode() & NetworkTableInstance.kNetModeStarting) != 0) {
+        Thread.sleep(100);
+      }
+      Thread.sleep(100);
+    } catch (InterruptedException ex) {
+      fail("interrupted while waiting for client to start");
+    }
+  }
+
+  @Test
+  @DisabledOnOs(OS.WINDOWS)
+  void testJNI() {
+    // set up the poller
+    int poller = NetworkTablesJNI.createConnectionListenerPoller(m_serverInst.getHandle());
+    assertNotSame(poller, 0, "bad poller handle");
+    int handle = NetworkTablesJNI.addPolledConnectionListener(poller, false);
+    assertNotSame(handle, 0, "bad listener handle");
+
+    // trigger a connect event
+    connect();
+
+    // get the event
+    assertTrue(m_serverInst.waitForConnectionListenerQueue(1.0));
+    ConnectionNotification[] events = null;
+    try {
+      events = NetworkTablesJNI.pollConnectionListenerTimeout(m_serverInst, poller, 0.0);
+    } catch (InterruptedException ex) {
+      Thread.currentThread().interrupt();
+      fail("unexpected interrupted exception" + ex);
+    }
+
+    assertNotNull(events);
+    assertEquals(1, events.length);
+    assertEquals(handle, events[0].listener);
+    assertTrue(events[0].connected);
+
+    // trigger a disconnect event
+    m_clientInst.stopClient();
+    try {
+      Thread.sleep(100);
+    } catch (InterruptedException ex) {
+      fail("interrupted while waiting for client to stop");
+    }
+
+    // get the event
+    assertTrue(m_serverInst.waitForConnectionListenerQueue(1.0));
+    try {
+      events = NetworkTablesJNI.pollConnectionListenerTimeout(m_serverInst, poller, 0.0);
+    } catch (InterruptedException ex) {
+      Thread.currentThread().interrupt();
+      fail("unexpected interrupted exception" + ex);
+    }
+
+    assertNotNull(events);
+    assertEquals(1, events.length);
+    assertEquals(handle, events[0].listener);
+    assertFalse(events[0].connected);
+
+  }
+
+  @ParameterizedTest
+  @DisabledOnOs(OS.WINDOWS)
+  @SuppressWarnings("PMD.AvoidUsingHardCodedIP")
+  @ValueSource(strings = { "127.0.0.1", "127.0.0.1 ", " 127.0.0.1 " })
+  void testThreaded(String address) {
+    m_serverInst.startServer("connectionlistenertest.ini", address, 10000);
+    List<ConnectionNotification> events = new ArrayList<>();
+    final int handle = m_serverInst.addConnectionListener(events::add, false);
+
+    // trigger a connect event
+    m_clientInst.startClient(address, 10000);
+
+    // wait for client to report it's started, then wait another 0.1 sec
+    try {
+      while ((m_clientInst.getNetworkMode() & NetworkTableInstance.kNetModeStarting) != 0) {
+        Thread.sleep(100);
+      }
+      Thread.sleep(100);
+    } catch (InterruptedException ex) {
+      fail("interrupted while waiting for client to start");
+    }
+    assertTrue(m_serverInst.waitForConnectionListenerQueue(1.0));
+
+    // get the event
+    assertEquals(1, events.size());
+    assertEquals(handle, events.get(0).listener);
+    assertTrue(events.get(0).connected);
+    events.clear();
+
+    // trigger a disconnect event
+    m_clientInst.stopClient();
+    try {
+      Thread.sleep(100);
+    } catch (InterruptedException ex) {
+      fail("interrupted while waiting for client to stop");
+    }
+
+    // get the event
+    assertTrue(m_serverInst.waitForConnectionListenerQueue(1.0));
+    assertEquals(1, events.size());
+    assertEquals(handle, events.get(0).listener);
+    assertFalse(events.get(0).connected);
+  }
+}
diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/EntryListenerTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/EntryListenerTest.java
new file mode 100644
index 0000000..80627a4
--- /dev/null
+++ b/ntcore/src/test/java/edu/wpi/first/networktables/EntryListenerTest.java
@@ -0,0 +1,91 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+class EntryListenerTest {
+  private NetworkTableInstance m_serverInst;
+  private NetworkTableInstance m_clientInst;
+
+  @BeforeEach
+  void setUp() {
+    m_serverInst = NetworkTableInstance.create();
+    m_serverInst.setNetworkIdentity("server");
+
+    m_clientInst = NetworkTableInstance.create();
+    m_clientInst.setNetworkIdentity("client");
+  }
+
+  @AfterEach
+  void tearDown() {
+    m_clientInst.close();
+    m_serverInst.close();
+  }
+
+  @SuppressWarnings("PMD.AvoidUsingHardCodedIP")
+  private void connect() {
+    m_serverInst.startServer("connectionlistenertest.ini", "127.0.0.1", 10000);
+    m_clientInst.startClient("127.0.0.1", 10000);
+
+    // Use connection listener to ensure we've connected
+    int poller = NetworkTablesJNI.createConnectionListenerPoller(m_clientInst.getHandle());
+    NetworkTablesJNI.addPolledConnectionListener(poller, false);
+    try {
+      if (NetworkTablesJNI.pollConnectionListenerTimeout(m_clientInst, poller, 1.0).length == 0) {
+        fail("client didn't connect to server");
+      }
+    } catch (InterruptedException ex) {
+      Thread.currentThread().interrupt();
+      fail("interrupted while waiting for server connection");
+    }
+  }
+
+  /**
+   * Test prefix with a new remote.
+   */
+  @Test
+  void testPrefixNewRemote() {
+    connect();
+    List<EntryNotification> events = new ArrayList<>();
+    final int handle = m_serverInst.addEntryListener("/foo", events::add,
+        EntryListenerFlags.kNew);
+
+    // Trigger an event
+    m_clientInst.getEntry("/foo/bar").setDouble(1.0);
+    m_clientInst.getEntry("/baz").setDouble(1.0);
+    m_clientInst.flush();
+    try {
+      Thread.sleep(100);
+    } catch (InterruptedException ex) {
+      fail("interrupted while waiting for entries to update");
+    }
+
+    assertTrue(m_serverInst.waitForEntryListenerQueue(1.0));
+
+    // Check the event
+    assertAll("Event",
+        () -> assertEquals(1, events.size()),
+        () -> assertEquals(handle, events.get(0).listener),
+        () -> assertEquals(m_serverInst.getEntry("/foo/bar"), events.get(0).getEntry()),
+        () -> assertEquals("/foo/bar", events.get(0).name),
+        () -> assertEquals(NetworkTableValue.makeDouble(1.0), events.get(0).value),
+        () -> assertEquals(EntryListenerFlags.kNew, events.get(0).flags)
+    );
+  }
+}
diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/JNITest.java b/ntcore/src/test/java/edu/wpi/first/networktables/JNITest.java
new file mode 100644
index 0000000..ef2b42b
--- /dev/null
+++ b/ntcore/src/test/java/edu/wpi/first/networktables/JNITest.java
@@ -0,0 +1,19 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+import org.junit.jupiter.api.Test;
+
+class JNITest {
+  @Test
+  void jniLinkTest() {
+    // Test to verify that the JNI test link works correctly.
+    int inst = NetworkTablesJNI.getDefaultInstance();
+    NetworkTablesJNI.flush(inst);
+  }
+}
diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/LoggerTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/LoggerTest.java
new file mode 100644
index 0000000..420f9dc
--- /dev/null
+++ b/ntcore/src/test/java/edu/wpi/first/networktables/LoggerTest.java
@@ -0,0 +1,53 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.fail;
+
+class LoggerTest {
+  private NetworkTableInstance m_clientInst;
+
+  @BeforeEach
+  protected void setUp() {
+    m_clientInst = NetworkTableInstance.create();
+  }
+
+  @AfterEach
+  protected void tearDown() {
+    m_clientInst.close();
+  }
+
+  @Test
+  @SuppressWarnings("PMD.AvoidUsingHardCodedIP")
+  void addMessageTest() {
+    List<LogMessage> msgs = new ArrayList<>();
+    m_clientInst.addLogger(msgs::add, LogMessage.kInfo, 100);
+
+    m_clientInst.startClient("127.0.0.1", 10000);
+
+    // wait for client to report it's started, then wait another 0.1 sec
+    try {
+      while ((m_clientInst.getNetworkMode() & NetworkTableInstance.kNetModeStarting) != 0) {
+        Thread.sleep(100);
+      }
+      Thread.sleep(100);
+    } catch (InterruptedException ex) {
+      fail("interrupted while waiting for client to start");
+    }
+
+    assertFalse(msgs.isEmpty());
+  }
+}
diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/NetworkTableTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/NetworkTableTest.java
new file mode 100644
index 0000000..e45f197
--- /dev/null
+++ b/ntcore/src/test/java/edu/wpi/first/networktables/NetworkTableTest.java
@@ -0,0 +1,80 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+package edu.wpi.first.networktables;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class NetworkTableTest {
+  private static Stream<Arguments> basenameKeyArguments() {
+    return Stream.of(
+        Arguments.of("simple", "simple"),
+        Arguments.of("simple", "one/two/many/simple"),
+        Arguments.of("simple", "//////an/////awful/key////simple")
+    );
+  }
+
+  @ParameterizedTest
+  @MethodSource("basenameKeyArguments")
+  void basenameKeyTest(final String expected, final String testString) {
+    assertEquals(expected, NetworkTable.basenameKey(testString));
+  }
+
+  private static Stream<Arguments> normalizeKeySlashArguments() {
+    return Stream.of(
+        Arguments.of("/", "///"),
+        Arguments.of("/no/normal/req", "/no/normal/req"),
+        Arguments.of("/no/leading/slash", "no/leading/slash"),
+        Arguments.of("/what/an/awful/key/", "//////what////an/awful/////key///")
+    );
+  }
+
+  @ParameterizedTest
+  @MethodSource("normalizeKeySlashArguments")
+  void normalizeKeySlashTest(final String expected, final String testString) {
+    assertEquals(expected, NetworkTable.normalizeKey(testString));
+  }
+
+  private static Stream<Arguments> normalizeKeyNoSlashArguments() {
+    return Stream.of(
+        Arguments.of("a", "a"),
+        Arguments.of("a", "///a"),
+        Arguments.of("leading/slash", "/leading/slash"),
+        Arguments.of("no/leading/slash", "no/leading/slash"),
+        Arguments.of("what/an/awful/key/", "//////what////an/awful/////key///")
+    );
+  }
+
+  @ParameterizedTest
+  @MethodSource("normalizeKeyNoSlashArguments")
+  void normalizeKeyNoSlashTest(final String expected, final String testString) {
+    assertEquals(expected, NetworkTable.normalizeKey(testString, false));
+  }
+
+  private static Stream<Arguments> getHierarchyArguments() {
+    return Stream.of(
+        Arguments.of(Arrays.asList("/"), ""),
+        Arguments.of(Arrays.asList("/"), "/"),
+        Arguments.of(Arrays.asList("/", "/foo", "/foo/bar", "/foo/bar/baz"), "/foo/bar/baz"),
+        Arguments.of(Arrays.asList("/", "/foo", "/foo/bar", "/foo/bar/"), "/foo/bar/")
+    );
+  }
+
+  @ParameterizedTest
+  @MethodSource("getHierarchyArguments")
+  void getHierarchyTest(final List<String> expected, final String testString) {
+    assertEquals(expected, NetworkTable.getHierarchy(testString));
+  }
+}
diff --git a/ntcore/src/test/native/cpp/ConnectionListenerTest.cpp b/ntcore/src/test/native/cpp/ConnectionListenerTest.cpp
new file mode 100644
index 0000000..a56e45c
--- /dev/null
+++ b/ntcore/src/test/native/cpp/ConnectionListenerTest.cpp
@@ -0,0 +1,108 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <chrono>
+#include <thread>
+
+#include "TestPrinters.h"
+#include "gtest/gtest.h"
+#include "ntcore_cpp.h"
+
+class ConnectionListenerTest : public ::testing::Test {
+ public:
+  ConnectionListenerTest()
+      : server_inst(nt::CreateInstance()), client_inst(nt::CreateInstance()) {
+    nt::SetNetworkIdentity(server_inst, "server");
+    nt::SetNetworkIdentity(client_inst, "client");
+  }
+
+  ~ConnectionListenerTest() override {
+    nt::DestroyInstance(server_inst);
+    nt::DestroyInstance(client_inst);
+  }
+
+  void Connect();
+
+ protected:
+  NT_Inst server_inst;
+  NT_Inst client_inst;
+};
+
+void ConnectionListenerTest::Connect() {
+  nt::StartServer(server_inst, "connectionlistenertest.ini", "127.0.0.1",
+                  10000);
+  nt::StartClient(client_inst, "127.0.0.1", 10000);
+
+  // wait for client to report it's started, then wait another 0.1 sec
+  while ((nt::GetNetworkMode(client_inst) & NT_NET_MODE_STARTING) != 0)
+    std::this_thread::sleep_for(std::chrono::milliseconds(100));
+  std::this_thread::sleep_for(std::chrono::milliseconds(100));
+}
+
+TEST_F(ConnectionListenerTest, Polled) {
+  // set up the poller
+  NT_ConnectionListenerPoller poller =
+      nt::CreateConnectionListenerPoller(server_inst);
+  ASSERT_NE(poller, 0u);
+  NT_ConnectionListener handle = nt::AddPolledConnectionListener(poller, false);
+  ASSERT_NE(handle, 0u);
+
+  // trigger a connect event
+  Connect();
+
+  // get the event
+  ASSERT_TRUE(nt::WaitForConnectionListenerQueue(server_inst, 1.0));
+  bool timed_out = false;
+  auto result = nt::PollConnectionListener(poller, 0.1, &timed_out);
+  EXPECT_FALSE(timed_out);
+  ASSERT_EQ(result.size(), 1u);
+  EXPECT_EQ(handle, result[0].listener);
+  EXPECT_TRUE(result[0].connected);
+
+  // trigger a disconnect event
+  nt::StopClient(client_inst);
+  std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+  // get the event
+  ASSERT_TRUE(nt::WaitForConnectionListenerQueue(server_inst, 1.0));
+  timed_out = false;
+  result = nt::PollConnectionListener(poller, 0.1, &timed_out);
+  EXPECT_FALSE(timed_out);
+  ASSERT_EQ(result.size(), 1u);
+  EXPECT_EQ(handle, result[0].listener);
+  EXPECT_FALSE(result[0].connected);
+
+  // trigger a disconnect event
+}
+
+TEST_F(ConnectionListenerTest, Threaded) {
+  std::vector<nt::ConnectionNotification> result;
+  auto handle = nt::AddConnectionListener(
+      server_inst,
+      [&](const nt::ConnectionNotification& event) { result.push_back(event); },
+      false);
+
+  // trigger a connect event
+  Connect();
+
+  ASSERT_TRUE(nt::WaitForConnectionListenerQueue(server_inst, 1.0));
+
+  // get the event
+  ASSERT_EQ(result.size(), 1u);
+  EXPECT_EQ(handle, result[0].listener);
+  EXPECT_TRUE(result[0].connected);
+  result.clear();
+
+  // trigger a disconnect event
+  nt::StopClient(client_inst);
+  std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+  // get the event
+  ASSERT_EQ(result.size(), 1u);
+  EXPECT_EQ(handle, result[0].listener);
+  EXPECT_FALSE(result[0].connected);
+}
diff --git a/ntcore/src/test/native/cpp/EntryListenerTest.cpp b/ntcore/src/test/native/cpp/EntryListenerTest.cpp
new file mode 100644
index 0000000..b7bf2f6
--- /dev/null
+++ b/ntcore/src/test/native/cpp/EntryListenerTest.cpp
@@ -0,0 +1,164 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <chrono>
+#include <thread>
+
+#include "TestPrinters.h"
+#include "ValueMatcher.h"
+#include "gtest/gtest.h"
+#include "ntcore_cpp.h"
+
+class EntryListenerTest : public ::testing::Test {
+ public:
+  EntryListenerTest()
+      : server_inst(nt::CreateInstance()), client_inst(nt::CreateInstance()) {
+    nt::SetNetworkIdentity(server_inst, "server");
+    nt::SetNetworkIdentity(client_inst, "client");
+#if 0
+    nt::AddLogger(server_inst,
+                  [](const nt::LogMessage& msg) {
+                    std::fprintf(stderr, "SERVER: %s\n", msg.message.c_str());
+                  },
+                  0, UINT_MAX);
+    nt::AddLogger(client_inst,
+                  [](const nt::LogMessage& msg) {
+                    std::fprintf(stderr, "CLIENT: %s\n", msg.message.c_str());
+                  },
+                  0, UINT_MAX);
+#endif
+  }
+
+  ~EntryListenerTest() override {
+    nt::DestroyInstance(server_inst);
+    nt::DestroyInstance(client_inst);
+  }
+
+  void Connect();
+
+ protected:
+  NT_Inst server_inst;
+  NT_Inst client_inst;
+};
+
+void EntryListenerTest::Connect() {
+  nt::StartServer(server_inst, "entrylistenertest.ini", "127.0.0.1", 10000);
+  nt::StartClient(client_inst, "127.0.0.1", 10000);
+
+  // Use connection listener to ensure we've connected
+  NT_ConnectionListenerPoller poller =
+      nt::CreateConnectionListenerPoller(server_inst);
+  nt::AddPolledConnectionListener(poller, false);
+  bool timed_out = false;
+  if (nt::PollConnectionListener(poller, 1.0, &timed_out).empty()) {
+    FAIL() << "client didn't connect to server";
+  }
+}
+
+TEST_F(EntryListenerTest, EntryNewLocal) {
+  std::vector<nt::EntryNotification> events;
+  auto handle = nt::AddEntryListener(
+      nt::GetEntry(server_inst, "/foo"),
+      [&](const nt::EntryNotification& event) { events.push_back(event); },
+      NT_NOTIFY_NEW | NT_NOTIFY_LOCAL);
+
+  // Trigger an event
+  nt::SetEntryValue(nt::GetEntry(server_inst, "/foo/bar"),
+                    nt::Value::MakeDouble(2.0));
+  nt::SetEntryValue(nt::GetEntry(server_inst, "/foo"),
+                    nt::Value::MakeDouble(1.0));
+
+  ASSERT_TRUE(nt::WaitForEntryListenerQueue(server_inst, 1.0));
+
+  // Check the event
+  ASSERT_EQ(events.size(), 1u);
+  ASSERT_EQ(events[0].listener, handle);
+  ASSERT_EQ(events[0].entry, nt::GetEntry(server_inst, "/foo"));
+  ASSERT_EQ(events[0].name, "/foo");
+  ASSERT_THAT(events[0].value, nt::ValueEq(nt::Value::MakeDouble(1.0)));
+  ASSERT_EQ(events[0].flags, (unsigned int)(NT_NOTIFY_NEW | NT_NOTIFY_LOCAL));
+}
+
+TEST_F(EntryListenerTest, DISABLED_EntryNewRemote) {
+  Connect();
+  if (HasFatalFailure()) return;
+  std::vector<nt::EntryNotification> events;
+  auto handle = nt::AddEntryListener(
+      nt::GetEntry(server_inst, "/foo"),
+      [&](const nt::EntryNotification& event) { events.push_back(event); },
+      NT_NOTIFY_NEW);
+
+  // Trigger an event
+  nt::SetEntryValue(nt::GetEntry(client_inst, "/foo/bar"),
+                    nt::Value::MakeDouble(2.0));
+  nt::SetEntryValue(nt::GetEntry(client_inst, "/foo"),
+                    nt::Value::MakeDouble(1.0));
+  nt::Flush(client_inst);
+  std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+  ASSERT_TRUE(nt::WaitForEntryListenerQueue(server_inst, 1.0));
+
+  // Check the event
+  ASSERT_EQ(events.size(), 1u);
+  ASSERT_EQ(events[0].listener, handle);
+  ASSERT_EQ(events[0].entry, nt::GetEntry(server_inst, "/foo"));
+  ASSERT_EQ(events[0].name, "/foo");
+  ASSERT_THAT(events[0].value, nt::ValueEq(nt::Value::MakeDouble(1.0)));
+  ASSERT_EQ(events[0].flags, NT_NOTIFY_NEW);
+}
+
+TEST_F(EntryListenerTest, PrefixNewLocal) {
+  std::vector<nt::EntryNotification> events;
+  auto handle = nt::AddEntryListener(
+      server_inst, "/foo",
+      [&](const nt::EntryNotification& event) { events.push_back(event); },
+      NT_NOTIFY_NEW | NT_NOTIFY_LOCAL);
+
+  // Trigger an event
+  nt::SetEntryValue(nt::GetEntry(server_inst, "/foo/bar"),
+                    nt::Value::MakeDouble(1.0));
+  nt::SetEntryValue(nt::GetEntry(server_inst, "/baz"),
+                    nt::Value::MakeDouble(1.0));
+
+  ASSERT_TRUE(nt::WaitForEntryListenerQueue(server_inst, 1.0));
+
+  // Check the event
+  ASSERT_EQ(events.size(), 1u);
+  ASSERT_EQ(events[0].listener, handle);
+  ASSERT_EQ(events[0].entry, nt::GetEntry(server_inst, "/foo/bar"));
+  ASSERT_EQ(events[0].name, "/foo/bar");
+  ASSERT_THAT(events[0].value, nt::ValueEq(nt::Value::MakeDouble(1.0)));
+  ASSERT_EQ(events[0].flags, (unsigned int)(NT_NOTIFY_NEW | NT_NOTIFY_LOCAL));
+}
+
+TEST_F(EntryListenerTest, DISABLED_PrefixNewRemote) {
+  Connect();
+  if (HasFatalFailure()) return;
+  std::vector<nt::EntryNotification> events;
+  auto handle = nt::AddEntryListener(
+      server_inst, "/foo",
+      [&](const nt::EntryNotification& event) { events.push_back(event); },
+      NT_NOTIFY_NEW);
+
+  // Trigger an event
+  nt::SetEntryValue(nt::GetEntry(client_inst, "/foo/bar"),
+                    nt::Value::MakeDouble(1.0));
+  nt::SetEntryValue(nt::GetEntry(client_inst, "/baz"),
+                    nt::Value::MakeDouble(1.0));
+  nt::Flush(client_inst);
+  std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+  ASSERT_TRUE(nt::WaitForEntryListenerQueue(server_inst, 1.0));
+
+  // Check the event
+  ASSERT_EQ(events.size(), 1u);
+  ASSERT_EQ(events[0].listener, handle);
+  ASSERT_EQ(events[0].entry, nt::GetEntry(server_inst, "/foo/bar"));
+  ASSERT_EQ(events[0].name, "/foo/bar");
+  ASSERT_THAT(events[0].value, nt::ValueEq(nt::Value::MakeDouble(1.0)));
+  ASSERT_EQ(events[0].flags, NT_NOTIFY_NEW);
+}
diff --git a/ntcore/src/test/native/cpp/EntryNotifierTest.cpp b/ntcore/src/test/native/cpp/EntryNotifierTest.cpp
new file mode 100644
index 0000000..4f1df77
--- /dev/null
+++ b/ntcore/src/test/native/cpp/EntryNotifierTest.cpp
@@ -0,0 +1,314 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <wpi/Logger.h>
+
+#include "EntryNotifier.h"
+#include "TestPrinters.h"
+#include "ValueMatcher.h"
+#include "gtest/gtest.h"
+
+using ::testing::AnyNumber;
+using ::testing::IsNull;
+using ::testing::Return;
+using ::testing::_;
+
+namespace nt {
+
+class EntryNotifierTest : public ::testing::Test {
+ public:
+  EntryNotifierTest() : notifier(1, logger) { notifier.Start(); }
+
+  void GenerateNotifications();
+
+ protected:
+  wpi::Logger logger;
+  EntryNotifier notifier;
+};
+
+void EntryNotifierTest::GenerateNotifications() {
+  // All flags combos that can be generated by Storage
+  static const unsigned int flags[] = {
+      // "normal" notifications
+      NT_NOTIFY_NEW, NT_NOTIFY_DELETE, NT_NOTIFY_UPDATE, NT_NOTIFY_FLAGS,
+      NT_NOTIFY_UPDATE | NT_NOTIFY_FLAGS,
+      // immediate notifications are always "new"
+      NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW,
+      // local notifications can be of any flag combo
+      NT_NOTIFY_LOCAL | NT_NOTIFY_NEW, NT_NOTIFY_LOCAL | NT_NOTIFY_DELETE,
+      NT_NOTIFY_LOCAL | NT_NOTIFY_UPDATE, NT_NOTIFY_LOCAL | NT_NOTIFY_FLAGS,
+      NT_NOTIFY_LOCAL | NT_NOTIFY_UPDATE | NT_NOTIFY_FLAGS};
+  // Generate across keys
+  static const char* keys[] = {"/foo/bar", "/baz", "/boo"};
+
+  auto val = Value::MakeDouble(1);
+
+  // Provide unique key indexes for each key
+  unsigned int keyindex = 5;
+  for (auto key : keys) {
+    for (auto flag : flags) {
+      notifier.NotifyEntry(keyindex, key, val, flag);
+    }
+    ++keyindex;
+  }
+}
+
+TEST_F(EntryNotifierTest, PollEntryMultiple) {
+  auto poller1 = notifier.CreatePoller();
+  auto poller2 = notifier.CreatePoller();
+  auto poller3 = notifier.CreatePoller();
+  auto h1 = notifier.AddPolled(poller1, 6, NT_NOTIFY_NEW);
+  auto h2 = notifier.AddPolled(poller2, 6, NT_NOTIFY_NEW);
+  auto h3 = notifier.AddPolled(poller3, 6, NT_NOTIFY_UPDATE);
+
+  ASSERT_FALSE(notifier.local_notifiers());
+
+  GenerateNotifications();
+
+  ASSERT_TRUE(notifier.WaitForQueue(1.0));
+  bool timed_out = false;
+  auto results1 = notifier.Poll(poller1, 0, &timed_out);
+  ASSERT_FALSE(timed_out);
+  auto results2 = notifier.Poll(poller2, 0, &timed_out);
+  ASSERT_FALSE(timed_out);
+  auto results3 = notifier.Poll(poller3, 0, &timed_out);
+  ASSERT_FALSE(timed_out);
+
+  ASSERT_EQ(results1.size(), 2u);
+  for (const auto& result : results1) {
+    SCOPED_TRACE(::testing::PrintToString(result));
+    EXPECT_EQ(Handle{result.listener}.GetIndex(), (int)h1);
+  }
+
+  ASSERT_EQ(results2.size(), 2u);
+  for (const auto& result : results2) {
+    SCOPED_TRACE(::testing::PrintToString(result));
+    EXPECT_EQ(Handle{result.listener}.GetIndex(), (int)h2);
+  }
+
+  ASSERT_EQ(results3.size(), 2u);
+  for (const auto& result : results3) {
+    SCOPED_TRACE(::testing::PrintToString(result));
+    EXPECT_EQ(Handle{result.listener}.GetIndex(), (int)h3);
+  }
+}
+
+TEST_F(EntryNotifierTest, PollEntryBasic) {
+  auto poller = notifier.CreatePoller();
+  auto g1 = notifier.AddPolled(poller, 6, NT_NOTIFY_NEW);
+  auto g2 = notifier.AddPolled(poller, 6, NT_NOTIFY_DELETE);
+  auto g3 = notifier.AddPolled(poller, 6, NT_NOTIFY_UPDATE);
+  auto g4 = notifier.AddPolled(poller, 6, NT_NOTIFY_FLAGS);
+
+  ASSERT_FALSE(notifier.local_notifiers());
+
+  GenerateNotifications();
+
+  ASSERT_TRUE(notifier.WaitForQueue(1.0));
+  bool timed_out = false;
+  auto results = notifier.Poll(poller, 0, &timed_out);
+  ASSERT_FALSE(timed_out);
+
+  int g1count = 0;
+  int g2count = 0;
+  int g3count = 0;
+  int g4count = 0;
+  for (const auto& result : results) {
+    SCOPED_TRACE(::testing::PrintToString(result));
+    EXPECT_EQ(result.name, "/baz");
+    EXPECT_THAT(result.value, ValueEq(Value::MakeDouble(1)));
+    EXPECT_EQ(Handle{result.entry}.GetType(), Handle::kEntry);
+    EXPECT_EQ(Handle{result.entry}.GetInst(), 1);
+    EXPECT_EQ(Handle{result.entry}.GetIndex(), 6);
+    EXPECT_EQ(Handle{result.listener}.GetType(), Handle::kEntryListener);
+    EXPECT_EQ(Handle{result.listener}.GetInst(), 1);
+    if (Handle{result.listener}.GetIndex() == static_cast<int>(g1)) {
+      ++g1count;
+      EXPECT_TRUE((result.flags & NT_NOTIFY_NEW) != 0);
+    } else if (Handle{result.listener}.GetIndex() == static_cast<int>(g2)) {
+      ++g2count;
+      EXPECT_TRUE((result.flags & NT_NOTIFY_DELETE) != 0);
+    } else if (Handle{result.listener}.GetIndex() == static_cast<int>(g3)) {
+      ++g3count;
+      EXPECT_TRUE((result.flags & NT_NOTIFY_UPDATE) != 0);
+    } else if (Handle{result.listener}.GetIndex() == static_cast<int>(g4)) {
+      ++g4count;
+      EXPECT_TRUE((result.flags & NT_NOTIFY_FLAGS) != 0);
+    } else {
+      ADD_FAILURE() << "unknown listener index";
+    }
+  }
+  EXPECT_EQ(g1count, 2);
+  EXPECT_EQ(g2count, 1);  // NT_NOTIFY_DELETE
+  EXPECT_EQ(g3count, 2);
+  EXPECT_EQ(g4count, 2);
+}
+
+TEST_F(EntryNotifierTest, PollEntryImmediate) {
+  auto poller = notifier.CreatePoller();
+  notifier.AddPolled(poller, 6, NT_NOTIFY_NEW | NT_NOTIFY_IMMEDIATE);
+  notifier.AddPolled(poller, 6, NT_NOTIFY_NEW);
+
+  ASSERT_FALSE(notifier.local_notifiers());
+
+  GenerateNotifications();
+
+  ASSERT_TRUE(notifier.WaitForQueue(1.0));
+  bool timed_out = false;
+  auto results = notifier.Poll(poller, 0, &timed_out);
+  ASSERT_FALSE(timed_out);
+  SCOPED_TRACE(::testing::PrintToString(results));
+  ASSERT_EQ(results.size(), 4u);
+}
+
+TEST_F(EntryNotifierTest, PollEntryLocal) {
+  auto poller = notifier.CreatePoller();
+  notifier.AddPolled(poller, 6, NT_NOTIFY_NEW | NT_NOTIFY_LOCAL);
+  notifier.AddPolled(poller, 6, NT_NOTIFY_NEW);
+
+  ASSERT_TRUE(notifier.local_notifiers());
+
+  GenerateNotifications();
+
+  ASSERT_TRUE(notifier.WaitForQueue(1.0));
+  bool timed_out = false;
+  auto results = notifier.Poll(poller, 0, &timed_out);
+  ASSERT_FALSE(timed_out);
+  SCOPED_TRACE(::testing::PrintToString(results));
+  ASSERT_EQ(results.size(), 6u);
+}
+
+TEST_F(EntryNotifierTest, PollPrefixMultiple) {
+  auto poller1 = notifier.CreatePoller();
+  auto poller2 = notifier.CreatePoller();
+  auto poller3 = notifier.CreatePoller();
+  auto h1 = notifier.AddPolled(poller1, "/foo", NT_NOTIFY_NEW);
+  auto h2 = notifier.AddPolled(poller2, "/foo", NT_NOTIFY_NEW);
+  auto h3 = notifier.AddPolled(poller3, "/foo", NT_NOTIFY_UPDATE);
+
+  ASSERT_FALSE(notifier.local_notifiers());
+
+  GenerateNotifications();
+
+  ASSERT_TRUE(notifier.WaitForQueue(1.0));
+  bool timed_out = false;
+  auto results1 = notifier.Poll(poller1, 0, &timed_out);
+  ASSERT_FALSE(timed_out);
+  auto results2 = notifier.Poll(poller2, 0, &timed_out);
+  ASSERT_FALSE(timed_out);
+  auto results3 = notifier.Poll(poller3, 0, &timed_out);
+  ASSERT_FALSE(timed_out);
+
+  ASSERT_EQ(results1.size(), 2u);
+  for (const auto& result : results1) {
+    SCOPED_TRACE(::testing::PrintToString(result));
+    EXPECT_EQ(Handle{result.listener}.GetIndex(), (int)h1);
+  }
+
+  ASSERT_EQ(results2.size(), 2u);
+  for (const auto& result : results2) {
+    SCOPED_TRACE(::testing::PrintToString(result));
+    EXPECT_EQ(Handle{result.listener}.GetIndex(), (int)h2);
+  }
+
+  ASSERT_EQ(results3.size(), 2u);
+  for (const auto& result : results3) {
+    SCOPED_TRACE(::testing::PrintToString(result));
+    EXPECT_EQ(Handle{result.listener}.GetIndex(), (int)h3);
+  }
+}
+
+TEST_F(EntryNotifierTest, PollPrefixBasic) {
+  auto poller = notifier.CreatePoller();
+  auto g1 = notifier.AddPolled(poller, "/foo", NT_NOTIFY_NEW);
+  auto g2 = notifier.AddPolled(poller, "/foo", NT_NOTIFY_DELETE);
+  auto g3 = notifier.AddPolled(poller, "/foo", NT_NOTIFY_UPDATE);
+  auto g4 = notifier.AddPolled(poller, "/foo", NT_NOTIFY_FLAGS);
+  notifier.AddPolled(poller, "/bar", NT_NOTIFY_NEW);
+  notifier.AddPolled(poller, "/bar", NT_NOTIFY_DELETE);
+  notifier.AddPolled(poller, "/bar", NT_NOTIFY_UPDATE);
+  notifier.AddPolled(poller, "/bar", NT_NOTIFY_FLAGS);
+
+  ASSERT_FALSE(notifier.local_notifiers());
+
+  GenerateNotifications();
+
+  ASSERT_TRUE(notifier.WaitForQueue(1.0));
+  bool timed_out = false;
+  auto results = notifier.Poll(poller, 0, &timed_out);
+  ASSERT_FALSE(timed_out);
+
+  int g1count = 0;
+  int g2count = 0;
+  int g3count = 0;
+  int g4count = 0;
+  for (const auto& result : results) {
+    SCOPED_TRACE(::testing::PrintToString(result));
+    EXPECT_TRUE(StringRef(result.name).startswith("/foo"));
+    EXPECT_THAT(result.value, ValueEq(Value::MakeDouble(1)));
+    EXPECT_EQ(Handle{result.entry}.GetType(), Handle::kEntry);
+    EXPECT_EQ(Handle{result.entry}.GetInst(), 1);
+    EXPECT_EQ(Handle{result.entry}.GetIndex(), 5);
+    EXPECT_EQ(Handle{result.listener}.GetType(), Handle::kEntryListener);
+    EXPECT_EQ(Handle{result.listener}.GetInst(), 1);
+    if (Handle{result.listener}.GetIndex() == static_cast<int>(g1)) {
+      ++g1count;
+      EXPECT_TRUE((result.flags & NT_NOTIFY_NEW) != 0);
+    } else if (Handle{result.listener}.GetIndex() == static_cast<int>(g2)) {
+      ++g2count;
+      EXPECT_TRUE((result.flags & NT_NOTIFY_DELETE) != 0);
+    } else if (Handle{result.listener}.GetIndex() == static_cast<int>(g3)) {
+      ++g3count;
+      EXPECT_TRUE((result.flags & NT_NOTIFY_UPDATE) != 0);
+    } else if (Handle{result.listener}.GetIndex() == static_cast<int>(g4)) {
+      ++g4count;
+      EXPECT_TRUE((result.flags & NT_NOTIFY_FLAGS) != 0);
+    } else {
+      ADD_FAILURE() << "unknown listener index";
+    }
+  }
+  EXPECT_EQ(g1count, 2);
+  EXPECT_EQ(g2count, 1);  // NT_NOTIFY_DELETE
+  EXPECT_EQ(g3count, 2);
+  EXPECT_EQ(g4count, 2);
+}
+
+TEST_F(EntryNotifierTest, PollPrefixImmediate) {
+  auto poller = notifier.CreatePoller();
+  notifier.AddPolled(poller, "/foo", NT_NOTIFY_NEW | NT_NOTIFY_IMMEDIATE);
+  notifier.AddPolled(poller, "/foo", NT_NOTIFY_NEW);
+
+  ASSERT_FALSE(notifier.local_notifiers());
+
+  GenerateNotifications();
+
+  ASSERT_TRUE(notifier.WaitForQueue(1.0));
+  bool timed_out = false;
+  auto results = notifier.Poll(poller, 0, &timed_out);
+  ASSERT_FALSE(timed_out);
+  SCOPED_TRACE(::testing::PrintToString(results));
+  ASSERT_EQ(results.size(), 4u);
+}
+
+TEST_F(EntryNotifierTest, PollPrefixLocal) {
+  auto poller = notifier.CreatePoller();
+  notifier.AddPolled(poller, "/foo", NT_NOTIFY_NEW | NT_NOTIFY_LOCAL);
+  notifier.AddPolled(poller, "/foo", NT_NOTIFY_NEW);
+
+  ASSERT_TRUE(notifier.local_notifiers());
+
+  GenerateNotifications();
+
+  ASSERT_TRUE(notifier.WaitForQueue(1.0));
+  bool timed_out = false;
+  auto results = notifier.Poll(poller, 0, &timed_out);
+  ASSERT_FALSE(timed_out);
+  SCOPED_TRACE(::testing::PrintToString(results));
+  ASSERT_EQ(results.size(), 6u);
+}
+
+}  // namespace nt
diff --git a/ntcore/src/test/native/cpp/MessageMatcher.cpp b/ntcore/src/test/native/cpp/MessageMatcher.cpp
new file mode 100644
index 0000000..35d4f8b
--- /dev/null
+++ b/ntcore/src/test/native/cpp/MessageMatcher.cpp
@@ -0,0 +1,52 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "MessageMatcher.h"
+
+namespace nt {
+
+bool MessageMatcher::MatchAndExplain(
+    std::shared_ptr<Message> msg,
+    ::testing::MatchResultListener* listener) const {
+  bool match = true;
+  if (!msg) return false;
+  if (msg->str() != goodmsg->str()) {
+    *listener << "str mismatch ";
+    match = false;
+  }
+  if ((!msg->value() && goodmsg->value()) ||
+      (msg->value() && !goodmsg->value()) ||
+      (msg->value() && goodmsg->value() &&
+       *msg->value() != *goodmsg->value())) {
+    *listener << "value mismatch ";
+    match = false;
+  }
+  if (msg->id() != goodmsg->id()) {
+    *listener << "id mismatch ";
+    match = false;
+  }
+  if (msg->flags() != goodmsg->flags()) {
+    *listener << "flags mismatch";
+    match = false;
+  }
+  if (msg->seq_num_uid() != goodmsg->seq_num_uid()) {
+    *listener << "seq_num_uid mismatch";
+    match = false;
+  }
+  return match;
+}
+
+void MessageMatcher::DescribeTo(::std::ostream* os) const {
+  PrintTo(goodmsg, os);
+}
+
+void MessageMatcher::DescribeNegationTo(::std::ostream* os) const {
+  *os << "is not equal to ";
+  PrintTo(goodmsg, os);
+}
+
+}  // namespace nt
diff --git a/ntcore/src/test/native/cpp/MessageMatcher.h b/ntcore/src/test/native/cpp/MessageMatcher.h
new file mode 100644
index 0000000..5b14334
--- /dev/null
+++ b/ntcore/src/test/native/cpp/MessageMatcher.h
@@ -0,0 +1,42 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_MESSAGEMATCHER_H_
+#define NTCORE_MESSAGEMATCHER_H_
+
+#include <memory>
+#include <ostream>
+
+#include "Message.h"
+#include "TestPrinters.h"
+#include "gmock/gmock.h"
+
+namespace nt {
+
+class MessageMatcher
+    : public ::testing::MatcherInterface<std::shared_ptr<Message>> {
+ public:
+  explicit MessageMatcher(std::shared_ptr<Message> goodmsg_)
+      : goodmsg(goodmsg_) {}
+
+  bool MatchAndExplain(std::shared_ptr<Message> msg,
+                       ::testing::MatchResultListener* listener) const override;
+  void DescribeTo(::std::ostream* os) const override;
+  void DescribeNegationTo(::std::ostream* os) const override;
+
+ private:
+  std::shared_ptr<Message> goodmsg;
+};
+
+inline ::testing::Matcher<std::shared_ptr<Message>> MessageEq(
+    std::shared_ptr<Message> goodmsg) {
+  return ::testing::MakeMatcher(new MessageMatcher(goodmsg));
+}
+
+}  // namespace nt
+
+#endif  // NTCORE_MESSAGEMATCHER_H_
diff --git a/ntcore/src/test/native/cpp/MockConnectionNotifier.h b/ntcore/src/test/native/cpp/MockConnectionNotifier.h
new file mode 100644
index 0000000..ddf8b2e
--- /dev/null
+++ b/ntcore/src/test/native/cpp/MockConnectionNotifier.h
@@ -0,0 +1,30 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_MOCKCONNECTIONNOTIFIER_H_
+#define NTCORE_MOCKCONNECTIONNOTIFIER_H_
+
+#include "IConnectionNotifier.h"
+#include "gmock/gmock.h"
+
+namespace nt {
+
+class MockConnectionNotifier : public IConnectionNotifier {
+ public:
+  MOCK_METHOD1(
+      Add,
+      unsigned int(
+          std::function<void(const ConnectionNotification& event)> callback));
+  MOCK_METHOD1(AddPolled, unsigned int(unsigned int poller_uid));
+  MOCK_METHOD3(NotifyConnection,
+               void(bool connected, const ConnectionInfo& conn_info,
+                    unsigned int only_listener));
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_MOCKCONNECTIONNOTIFIER_H_
diff --git a/ntcore/src/test/native/cpp/MockDispatcher.h b/ntcore/src/test/native/cpp/MockDispatcher.h
new file mode 100644
index 0000000..10af839
--- /dev/null
+++ b/ntcore/src/test/native/cpp/MockDispatcher.h
@@ -0,0 +1,27 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_MOCKDISPATCHER_H_
+#define NTCORE_MOCKDISPATCHER_H_
+
+#include <memory>
+
+#include "IDispatcher.h"
+#include "gmock/gmock.h"
+
+namespace nt {
+
+class MockDispatcher : public IDispatcher {
+ public:
+  MOCK_METHOD3(QueueOutgoing,
+               void(std::shared_ptr<Message> msg, INetworkConnection* only,
+                    INetworkConnection* except));
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_MOCKDISPATCHER_H_
diff --git a/ntcore/src/test/native/cpp/MockEntryNotifier.h b/ntcore/src/test/native/cpp/MockEntryNotifier.h
new file mode 100644
index 0000000..2f078cb
--- /dev/null
+++ b/ntcore/src/test/native/cpp/MockEntryNotifier.h
@@ -0,0 +1,43 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_MOCKENTRYNOTIFIER_H_
+#define NTCORE_MOCKENTRYNOTIFIER_H_
+
+#include <memory>
+
+#include "IEntryNotifier.h"
+#include "gmock/gmock.h"
+
+namespace nt {
+
+class MockEntryNotifier : public IEntryNotifier {
+ public:
+  MOCK_CONST_METHOD0(local_notifiers, bool());
+  MOCK_METHOD3(
+      Add,
+      unsigned int(std::function<void(const EntryNotification& event)> callback,
+                   wpi::StringRef prefix, unsigned int flags));
+  MOCK_METHOD3(
+      Add,
+      unsigned int(std::function<void(const EntryNotification& event)> callback,
+                   unsigned int local_id, unsigned int flags));
+  MOCK_METHOD3(AddPolled,
+               unsigned int(unsigned int poller_uid, wpi::StringRef prefix,
+                            unsigned int flags));
+  MOCK_METHOD3(AddPolled,
+               unsigned int(unsigned int poller_uid, unsigned int local_id,
+                            unsigned int flags));
+  MOCK_METHOD5(NotifyEntry,
+               void(unsigned int local_id, StringRef name,
+                    std::shared_ptr<Value> value, unsigned int flags,
+                    unsigned int only_listener));
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_MOCKENTRYNOTIFIER_H_
diff --git a/ntcore/src/test/native/cpp/MockNetworkConnection.h b/ntcore/src/test/native/cpp/MockNetworkConnection.h
new file mode 100644
index 0000000..52c917d
--- /dev/null
+++ b/ntcore/src/test/native/cpp/MockNetworkConnection.h
@@ -0,0 +1,34 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_MOCKNETWORKCONNECTION_H_
+#define NTCORE_MOCKNETWORKCONNECTION_H_
+
+#include <memory>
+
+#include "INetworkConnection.h"
+#include "gmock/gmock.h"
+
+namespace nt {
+
+class MockNetworkConnection : public INetworkConnection {
+ public:
+  MOCK_CONST_METHOD0(info, ConnectionInfo());
+
+  MOCK_METHOD1(QueueOutgoing, void(std::shared_ptr<Message> msg));
+  MOCK_METHOD1(PostOutgoing, void(bool keep_alive));
+
+  MOCK_CONST_METHOD0(proto_rev, unsigned int());
+  MOCK_METHOD1(set_proto_rev, void(unsigned int proto_rev));
+
+  MOCK_CONST_METHOD0(state, State());
+  MOCK_METHOD1(set_state, void(State state));
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_MOCKNETWORKCONNECTION_H_
diff --git a/ntcore/src/test/native/cpp/MockRpcServer.h b/ntcore/src/test/native/cpp/MockRpcServer.h
new file mode 100644
index 0000000..6e9d970
--- /dev/null
+++ b/ntcore/src/test/native/cpp/MockRpcServer.h
@@ -0,0 +1,29 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_MOCKRPCSERVER_H_
+#define NTCORE_MOCKRPCSERVER_H_
+
+#include "IRpcServer.h"
+#include "gmock/gmock.h"
+
+namespace nt {
+
+class MockRpcServer : public IRpcServer {
+ public:
+  MOCK_METHOD0(Start, void());
+  MOCK_METHOD1(RemoveRpc, void(unsigned int rpc_uid));
+  MOCK_METHOD7(ProcessRpc,
+               void(unsigned int local_id, unsigned int call_uid,
+                    StringRef name, StringRef params,
+                    const ConnectionInfo& conn, SendResponseFunc send_response,
+                    unsigned int rpc_uid));
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_MOCKRPCSERVER_H_
diff --git a/ntcore/src/test/native/cpp/NetworkTableTest.cpp b/ntcore/src/test/native/cpp/NetworkTableTest.cpp
new file mode 100644
index 0000000..d9a2743
--- /dev/null
+++ b/ntcore/src/test/native/cpp/NetworkTableTest.cpp
@@ -0,0 +1,91 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "TestPrinters.h"
+#include "gtest/gtest.h"
+#include "networktables/NetworkTable.h"
+#include "networktables/NetworkTableInstance.h"
+
+class NetworkTableTest : public ::testing::Test {};
+
+TEST_F(NetworkTableTest, BasenameKey) {
+  EXPECT_EQ("simple", NetworkTable::BasenameKey("simple"));
+  EXPECT_EQ("simple", NetworkTable::BasenameKey("one/two/many/simple"));
+  EXPECT_EQ("simple",
+            NetworkTable::BasenameKey("//////an/////awful/key////simple"));
+}
+
+TEST_F(NetworkTableTest, NormalizeKeySlash) {
+  EXPECT_EQ("/", NetworkTable::NormalizeKey("///"));
+  EXPECT_EQ("/no/normal/req", NetworkTable::NormalizeKey("/no/normal/req"));
+  EXPECT_EQ("/no/leading/slash",
+            NetworkTable::NormalizeKey("no/leading/slash"));
+  EXPECT_EQ("/what/an/awful/key/",
+            NetworkTable::NormalizeKey("//////what////an/awful/////key///"));
+}
+
+TEST_F(NetworkTableTest, NormalizeKeyNoSlash) {
+  EXPECT_EQ("a", NetworkTable::NormalizeKey("a", false));
+  EXPECT_EQ("a", NetworkTable::NormalizeKey("///a", false));
+  EXPECT_EQ("leading/slash",
+            NetworkTable::NormalizeKey("/leading/slash", false));
+  EXPECT_EQ("no/leading/slash",
+            NetworkTable::NormalizeKey("no/leading/slash", false));
+  EXPECT_EQ(
+      "what/an/awful/key/",
+      NetworkTable::NormalizeKey("//////what////an/awful/////key///", false));
+}
+
+TEST_F(NetworkTableTest, GetHierarchyEmpty) {
+  std::vector<std::string> expected{"/"};
+  ASSERT_EQ(expected, NetworkTable::GetHierarchy(""));
+}
+
+TEST_F(NetworkTableTest, GetHierarchyRoot) {
+  std::vector<std::string> expected{"/"};
+  ASSERT_EQ(expected, NetworkTable::GetHierarchy("/"));
+}
+
+TEST_F(NetworkTableTest, GetHierarchyNormal) {
+  std::vector<std::string> expected{"/", "/foo", "/foo/bar", "/foo/bar/baz"};
+  ASSERT_EQ(expected, NetworkTable::GetHierarchy("/foo/bar/baz"));
+}
+
+TEST_F(NetworkTableTest, GetHierarchyTrailingSlash) {
+  std::vector<std::string> expected{"/", "/foo", "/foo/bar", "/foo/bar/"};
+  ASSERT_EQ(expected, NetworkTable::GetHierarchy("/foo/bar/"));
+}
+
+TEST_F(NetworkTableTest, ContainsKey) {
+  auto inst = nt::NetworkTableInstance::Create();
+  auto nt = inst.GetTable("containskey");
+  ASSERT_FALSE(nt->ContainsKey("testkey"));
+  nt->PutNumber("testkey", 5);
+  ASSERT_TRUE(nt->ContainsKey("testkey"));
+  ASSERT_TRUE(inst.GetEntry("/containskey/testkey").Exists());
+  ASSERT_FALSE(inst.GetEntry("containskey/testkey").Exists());
+}
+
+TEST_F(NetworkTableTest, LeadingSlash) {
+  auto inst = nt::NetworkTableInstance::Create();
+  auto nt = inst.GetTable("leadingslash");
+  auto nt2 = inst.GetTable("/leadingslash");
+  ASSERT_FALSE(nt->ContainsKey("testkey"));
+  nt2->PutNumber("testkey", 5);
+  ASSERT_TRUE(nt->ContainsKey("testkey"));
+  ASSERT_TRUE(inst.GetEntry("/leadingslash/testkey").Exists());
+}
+
+TEST_F(NetworkTableTest, EmptyOrNoSlash) {
+  auto inst = nt::NetworkTableInstance::Create();
+  auto nt = inst.GetTable("/");
+  auto nt2 = inst.GetTable("");
+  ASSERT_FALSE(nt->ContainsKey("testkey"));
+  nt2->PutNumber("testkey", 5);
+  ASSERT_TRUE(nt->ContainsKey("testkey"));
+  ASSERT_TRUE(inst.GetEntry("/testkey").Exists());
+}
diff --git a/ntcore/src/test/native/cpp/StorageTest.cpp b/ntcore/src/test/native/cpp/StorageTest.cpp
new file mode 100644
index 0000000..5a6982b
--- /dev/null
+++ b/ntcore/src/test/native/cpp/StorageTest.cpp
@@ -0,0 +1,987 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "StorageTest.h"
+
+#include <wpi/raw_istream.h>
+#include <wpi/raw_ostream.h>
+
+#include "MessageMatcher.h"
+#include "MockNetworkConnection.h"
+#include "Storage.h"
+#include "TestPrinters.h"
+#include "ValueMatcher.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+using ::testing::AnyNumber;
+using ::testing::IsNull;
+using ::testing::Return;
+using ::testing::_;
+
+namespace nt {
+
+class StorageTestEmpty : public StorageTest,
+                         public ::testing::TestWithParam<bool> {
+ public:
+  StorageTestEmpty() {
+    HookOutgoing(GetParam());
+    EXPECT_CALL(notifier, local_notifiers())
+        .Times(AnyNumber())
+        .WillRepeatedly(Return(true));
+  }
+};
+
+class StorageTestPopulateOne : public StorageTestEmpty {
+ public:
+  StorageTestPopulateOne() {
+    EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
+    EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
+    EXPECT_CALL(notifier, local_notifiers())
+        .Times(AnyNumber())
+        .WillRepeatedly(Return(false));
+    storage.SetEntryTypeValue("foo", Value::MakeBoolean(true));
+    ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
+    ::testing::Mock::VerifyAndClearExpectations(&notifier);
+    EXPECT_CALL(notifier, local_notifiers())
+        .Times(AnyNumber())
+        .WillRepeatedly(Return(true));
+  }
+};
+
+class StorageTestPopulated : public StorageTestEmpty {
+ public:
+  StorageTestPopulated() {
+    EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
+    EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
+    EXPECT_CALL(notifier, local_notifiers())
+        .Times(AnyNumber())
+        .WillRepeatedly(Return(false));
+    storage.SetEntryTypeValue("foo", Value::MakeBoolean(true));
+    storage.SetEntryTypeValue("foo2", Value::MakeDouble(0.0));
+    storage.SetEntryTypeValue("bar", Value::MakeDouble(1.0));
+    storage.SetEntryTypeValue("bar2", Value::MakeBoolean(false));
+    ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
+    ::testing::Mock::VerifyAndClearExpectations(&notifier);
+    EXPECT_CALL(notifier, local_notifiers())
+        .Times(AnyNumber())
+        .WillRepeatedly(Return(true));
+  }
+};
+
+class StorageTestPersistent : public StorageTestEmpty {
+ public:
+  StorageTestPersistent() {
+    EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
+    EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
+    EXPECT_CALL(notifier, local_notifiers())
+        .Times(AnyNumber())
+        .WillRepeatedly(Return(false));
+    storage.SetEntryTypeValue("boolean/true", Value::MakeBoolean(true));
+    storage.SetEntryTypeValue("boolean/false", Value::MakeBoolean(false));
+    storage.SetEntryTypeValue("double/neg", Value::MakeDouble(-1.5));
+    storage.SetEntryTypeValue("double/zero", Value::MakeDouble(0.0));
+    storage.SetEntryTypeValue("double/big", Value::MakeDouble(1.3e8));
+    storage.SetEntryTypeValue("string/empty", Value::MakeString(""));
+    storage.SetEntryTypeValue("string/normal", Value::MakeString("hello"));
+    storage.SetEntryTypeValue("string/special",
+                              Value::MakeString(StringRef("\0\3\5\n", 4)));
+    storage.SetEntryTypeValue("raw/empty", Value::MakeRaw(""));
+    storage.SetEntryTypeValue("raw/normal", Value::MakeRaw("hello"));
+    storage.SetEntryTypeValue("raw/special",
+                              Value::MakeRaw(StringRef("\0\3\5\n", 4)));
+    storage.SetEntryTypeValue("booleanarr/empty",
+                              Value::MakeBooleanArray(std::vector<int>{}));
+    storage.SetEntryTypeValue("booleanarr/one",
+                              Value::MakeBooleanArray(std::vector<int>{1}));
+    storage.SetEntryTypeValue("booleanarr/two",
+                              Value::MakeBooleanArray(std::vector<int>{1, 0}));
+    storage.SetEntryTypeValue("doublearr/empty",
+                              Value::MakeDoubleArray(std::vector<double>{}));
+    storage.SetEntryTypeValue("doublearr/one",
+                              Value::MakeDoubleArray(std::vector<double>{0.5}));
+    storage.SetEntryTypeValue(
+        "doublearr/two",
+        Value::MakeDoubleArray(std::vector<double>{0.5, -0.25}));
+    storage.SetEntryTypeValue(
+        "stringarr/empty", Value::MakeStringArray(std::vector<std::string>{}));
+    storage.SetEntryTypeValue(
+        "stringarr/one",
+        Value::MakeStringArray(std::vector<std::string>{"hello"}));
+    storage.SetEntryTypeValue(
+        "stringarr/two",
+        Value::MakeStringArray(std::vector<std::string>{"hello", "world\n"}));
+    storage.SetEntryTypeValue(StringRef("\0\3\5\n", 4),
+                              Value::MakeBoolean(true));
+    storage.SetEntryTypeValue("=", Value::MakeBoolean(true));
+    ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
+    ::testing::Mock::VerifyAndClearExpectations(&notifier);
+    EXPECT_CALL(notifier, local_notifiers())
+        .Times(AnyNumber())
+        .WillRepeatedly(Return(true));
+  }
+};
+
+class MockLoadWarn {
+ public:
+  MOCK_METHOD2(Warn, void(size_t line, wpi::StringRef msg));
+};
+
+TEST_P(StorageTestEmpty, Construct) {
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestEmpty, StorageEntryInit) {
+  auto entry = GetEntry("foo");
+  EXPECT_FALSE(entry->value);
+  EXPECT_EQ(0u, entry->flags);
+  EXPECT_EQ("foobar", entry->name);  // since GetEntry uses the tmp_entry.
+  EXPECT_EQ(0xffffu, entry->id);
+  EXPECT_EQ(SequenceNumber(), entry->seq_num);
+}
+
+TEST_P(StorageTestEmpty, GetEntryValueNotExist) {
+  EXPECT_FALSE(storage.GetEntryValue("foo"));
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestEmpty, GetEntryValueExist) {
+  auto value = Value::MakeBoolean(true);
+  EXPECT_CALL(dispatcher, QueueOutgoing(_, IsNull(), IsNull()));
+  EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _));
+  storage.SetEntryTypeValue("foo", value);
+  EXPECT_EQ(value, storage.GetEntryValue("foo"));
+}
+
+TEST_P(StorageTestEmpty, SetEntryTypeValueAssignNew) {
+  // brand new entry
+  auto value = Value::MakeBoolean(true);
+  // id assigned if server
+  EXPECT_CALL(dispatcher,
+              QueueOutgoing(MessageEq(Message::EntryAssign(
+                                "foo", GetParam() ? 0 : 0xffff, 1, value, 0)),
+                            IsNull(), IsNull()));
+  EXPECT_CALL(notifier, NotifyEntry(0, StringRef("foo"), value,
+                                    NT_NOTIFY_NEW | NT_NOTIFY_LOCAL, UINT_MAX));
+
+  storage.SetEntryTypeValue("foo", value);
+  EXPECT_EQ(value, GetEntry("foo")->value);
+  if (GetParam()) {
+    ASSERT_EQ(1u, idmap().size());
+    EXPECT_EQ(value, idmap()[0]->value);
+  } else {
+    EXPECT_TRUE(idmap().empty());
+  }
+}
+
+TEST_P(StorageTestPopulateOne, SetEntryTypeValueAssignTypeChange) {
+  // update with different type results in assignment message
+  auto value = Value::MakeDouble(0.0);
+
+  // id assigned if server; seq_num incremented
+  EXPECT_CALL(dispatcher,
+              QueueOutgoing(MessageEq(Message::EntryAssign(
+                                "foo", GetParam() ? 0 : 0xffff, 2, value, 0)),
+                            IsNull(), IsNull()));
+  EXPECT_CALL(notifier,
+              NotifyEntry(0, StringRef("foo"), value,
+                          NT_NOTIFY_UPDATE | NT_NOTIFY_LOCAL, UINT_MAX));
+
+  storage.SetEntryTypeValue("foo", value);
+  EXPECT_EQ(value, GetEntry("foo")->value);
+}
+
+TEST_P(StorageTestPopulateOne, SetEntryTypeValueEqualValue) {
+  // update with same type and same value: change value contents but no update
+  // message is issued (minimizing bandwidth usage)
+  auto value = Value::MakeBoolean(true);
+  storage.SetEntryTypeValue("foo", value);
+  EXPECT_EQ(value, GetEntry("foo")->value);
+}
+
+TEST_P(StorageTestPopulated, SetEntryTypeValueDifferentValue) {
+  // update with same type and different value results in value update message
+  auto value = Value::MakeDouble(1.0);
+
+  // client shouldn't send an update as id not assigned yet
+  if (GetParam()) {
+    // id assigned if server; seq_num incremented
+    EXPECT_CALL(dispatcher,
+                QueueOutgoing(MessageEq(Message::EntryUpdate(1, 2, value)),
+                              IsNull(), IsNull()));
+  }
+  EXPECT_CALL(notifier,
+              NotifyEntry(1, StringRef("foo2"), value,
+                          NT_NOTIFY_UPDATE | NT_NOTIFY_LOCAL, UINT_MAX));
+  storage.SetEntryTypeValue("foo2", value);
+  EXPECT_EQ(value, GetEntry("foo2")->value);
+
+  if (!GetParam()) {
+    // seq_num should still be incremented
+    EXPECT_EQ(2u, GetEntry("foo2")->seq_num.value());
+  }
+}
+
+TEST_P(StorageTestEmpty, SetEntryTypeValueEmptyName) {
+  auto value = Value::MakeBoolean(true);
+  storage.SetEntryTypeValue("", value);
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestEmpty, SetEntryTypeValueEmptyValue) {
+  storage.SetEntryTypeValue("foo", nullptr);
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestEmpty, SetEntryValueAssignNew) {
+  // brand new entry
+  auto value = Value::MakeBoolean(true);
+
+  // id assigned if server
+  EXPECT_CALL(dispatcher,
+              QueueOutgoing(MessageEq(Message::EntryAssign(
+                                "foo", GetParam() ? 0 : 0xffff, 1, value, 0)),
+                            IsNull(), IsNull()));
+  EXPECT_CALL(notifier, NotifyEntry(0, StringRef("foo"), value,
+                                    NT_NOTIFY_NEW | NT_NOTIFY_LOCAL, UINT_MAX));
+
+  EXPECT_TRUE(storage.SetEntryValue("foo", value));
+  EXPECT_EQ(value, GetEntry("foo")->value);
+}
+
+TEST_P(StorageTestPopulateOne, SetEntryValueAssignTypeChange) {
+  // update with different type results in error and no message or notification
+  auto value = Value::MakeDouble(0.0);
+  EXPECT_FALSE(storage.SetEntryValue("foo", value));
+  auto entry = GetEntry("foo");
+  EXPECT_NE(value, entry->value);
+}
+
+TEST_P(StorageTestPopulateOne, SetEntryValueEqualValue) {
+  // update with same type and same value: change value contents but no update
+  // message is issued (minimizing bandwidth usage)
+  auto value = Value::MakeBoolean(true);
+  EXPECT_TRUE(storage.SetEntryValue("foo", value));
+  auto entry = GetEntry("foo");
+  EXPECT_EQ(value, entry->value);
+}
+
+TEST_P(StorageTestPopulated, SetEntryValueDifferentValue) {
+  // update with same type and different value results in value update message
+  auto value = Value::MakeDouble(1.0);
+
+  // client shouldn't send an update as id not assigned yet
+  if (GetParam()) {
+    // id assigned if server; seq_num incremented
+    EXPECT_CALL(dispatcher,
+                QueueOutgoing(MessageEq(Message::EntryUpdate(1, 2, value)),
+                              IsNull(), IsNull()));
+  }
+  EXPECT_CALL(notifier,
+              NotifyEntry(1, StringRef("foo2"), value,
+                          NT_NOTIFY_UPDATE | NT_NOTIFY_LOCAL, UINT_MAX));
+
+  EXPECT_TRUE(storage.SetEntryValue("foo2", value));
+  auto entry = GetEntry("foo2");
+  EXPECT_EQ(value, entry->value);
+
+  if (!GetParam()) {
+    // seq_num should still be incremented
+    EXPECT_EQ(2u, GetEntry("foo2")->seq_num.value());
+  }
+}
+
+TEST_P(StorageTestEmpty, SetEntryValueEmptyName) {
+  auto value = Value::MakeBoolean(true);
+  EXPECT_TRUE(storage.SetEntryValue("", value));
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestEmpty, SetEntryValueEmptyValue) {
+  EXPECT_TRUE(storage.SetEntryValue("foo", nullptr));
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestEmpty, SetDefaultEntryAssignNew) {
+  // brand new entry
+  auto value = Value::MakeBoolean(true);
+
+  // id assigned if server
+  EXPECT_CALL(dispatcher,
+              QueueOutgoing(MessageEq(Message::EntryAssign(
+                                "foo", GetParam() ? 0 : 0xffff, 1, value, 0)),
+                            IsNull(), IsNull()));
+  EXPECT_CALL(notifier, NotifyEntry(0, StringRef("foo"), value,
+                                    NT_NOTIFY_NEW | NT_NOTIFY_LOCAL, UINT_MAX));
+
+  auto ret_val = storage.SetDefaultEntryValue("foo", value);
+  EXPECT_TRUE(ret_val);
+  EXPECT_EQ(value, GetEntry("foo")->value);
+}
+
+TEST_P(StorageTestPopulateOne, SetDefaultEntryExistsSameType) {
+  // existing entry
+  auto value = Value::MakeBoolean(true);
+  auto ret_val = storage.SetDefaultEntryValue("foo", value);
+  EXPECT_TRUE(ret_val);
+  EXPECT_NE(value, GetEntry("foo")->value);
+}
+
+TEST_P(StorageTestPopulateOne, SetDefaultEntryExistsDifferentType) {
+  // existing entry is boolean
+  auto value = Value::MakeDouble(2.0);
+  auto ret_val = storage.SetDefaultEntryValue("foo", value);
+  EXPECT_FALSE(ret_val);
+  // should not have updated value in table if it already existed.
+  EXPECT_NE(value, GetEntry("foo")->value);
+}
+
+TEST_P(StorageTestEmpty, SetDefaultEntryEmptyName) {
+  auto value = Value::MakeBoolean(true);
+  auto ret_val = storage.SetDefaultEntryValue("", value);
+  EXPECT_FALSE(ret_val);
+  auto entry = GetEntry("foo");
+  EXPECT_FALSE(entry->value);
+  EXPECT_EQ(0u, entry->flags);
+  EXPECT_EQ("foobar", entry->name);  // since GetEntry uses the tmp_entry.
+  EXPECT_EQ(0xffffu, entry->id);
+  EXPECT_EQ(SequenceNumber(), entry->seq_num);
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestEmpty, SetDefaultEntryEmptyValue) {
+  auto value = Value::MakeBoolean(true);
+  auto ret_val = storage.SetDefaultEntryValue("", nullptr);
+  EXPECT_FALSE(ret_val);
+  auto entry = GetEntry("foo");
+  EXPECT_FALSE(entry->value);
+  EXPECT_EQ(0u, entry->flags);
+  EXPECT_EQ("foobar", entry->name);  // since GetEntry uses the tmp_entry.
+  EXPECT_EQ(0xffffu, entry->id);
+  EXPECT_EQ(SequenceNumber(), entry->seq_num);
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestPopulated, SetDefaultEntryEmptyName) {
+  auto value = Value::MakeBoolean(true);
+  auto ret_val = storage.SetDefaultEntryValue("", value);
+  EXPECT_FALSE(ret_val);
+  // assert that no entries get added
+  EXPECT_EQ(4u, entries().size());
+  if (GetParam())
+    EXPECT_EQ(4u, idmap().size());
+  else
+    EXPECT_EQ(0u, idmap().size());
+}
+
+TEST_P(StorageTestPopulated, SetDefaultEntryEmptyValue) {
+  auto value = Value::MakeBoolean(true);
+  auto ret_val = storage.SetDefaultEntryValue("", nullptr);
+  EXPECT_FALSE(ret_val);
+  // assert that no entries get added
+  EXPECT_EQ(4u, entries().size());
+  if (GetParam())
+    EXPECT_EQ(4u, idmap().size());
+  else
+    EXPECT_EQ(0u, idmap().size());
+}
+
+TEST_P(StorageTestEmpty, SetEntryFlagsNew) {
+  // flags setting doesn't create an entry
+  storage.SetEntryFlags("foo", 0u);
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestPopulateOne, SetEntryFlagsEqualValue) {
+  // update with same value: no update message is issued (minimizing bandwidth
+  // usage)
+  storage.SetEntryFlags("foo", 0u);
+  auto entry = GetEntry("foo");
+  EXPECT_EQ(0u, entry->flags);
+}
+
+TEST_P(StorageTestPopulated, SetEntryFlagsDifferentValue) {
+  // update with different value results in flags update message
+  // client shouldn't send an update as id not assigned yet
+  if (GetParam()) {
+    // id assigned as this is the server
+    EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::FlagsUpdate(1, 1)),
+                                          IsNull(), IsNull()));
+  }
+  EXPECT_CALL(notifier,
+              NotifyEntry(1, StringRef("foo2"), _,
+                          NT_NOTIFY_FLAGS | NT_NOTIFY_LOCAL, UINT_MAX));
+  storage.SetEntryFlags("foo2", 1u);
+  EXPECT_EQ(1u, GetEntry("foo2")->flags);
+}
+
+TEST_P(StorageTestEmpty, SetEntryFlagsEmptyName) {
+  storage.SetEntryFlags("", 0u);
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestEmpty, GetEntryFlagsNotExist) {
+  EXPECT_EQ(0u, storage.GetEntryFlags("foo"));
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestPopulateOne, GetEntryFlagsExist) {
+  EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
+  EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _));
+  storage.SetEntryFlags("foo", 1u);
+  ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
+  EXPECT_EQ(1u, storage.GetEntryFlags("foo"));
+}
+
+TEST_P(StorageTestEmpty, DeleteEntryNotExist) { storage.DeleteEntry("foo"); }
+
+TEST_P(StorageTestPopulated, DeleteEntryExist) {
+  // client shouldn't send an update as id not assigned yet
+  if (GetParam()) {
+    // id assigned as this is the server
+    EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::EntryDelete(1)),
+                                          IsNull(), IsNull()));
+  }
+  EXPECT_CALL(notifier,
+              NotifyEntry(1, StringRef("foo2"), ValueEq(Value::MakeDouble(0)),
+                          NT_NOTIFY_DELETE | NT_NOTIFY_LOCAL, UINT_MAX));
+
+  storage.DeleteEntry("foo2");
+  ASSERT_EQ(1u, entries().count("foo2"));
+  EXPECT_EQ(nullptr, entries()["foo2"]->value);
+  EXPECT_EQ(0xffffu, entries()["foo2"]->id);
+  EXPECT_FALSE(entries()["foo2"]->local_write);
+  if (GetParam()) {
+    ASSERT_TRUE(idmap().size() >= 2);
+    EXPECT_FALSE(idmap()[1]);
+  }
+}
+
+TEST_P(StorageTestEmpty, DeleteAllEntriesEmpty) {
+  storage.DeleteAllEntries();
+  ASSERT_TRUE(entries().empty());
+}
+
+TEST_P(StorageTestPopulated, DeleteAllEntries) {
+  EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::ClearEntries()),
+                                        IsNull(), IsNull()));
+  EXPECT_CALL(notifier, NotifyEntry(_, _, _, NT_NOTIFY_DELETE | NT_NOTIFY_LOCAL,
+                                    UINT_MAX))
+      .Times(4);
+
+  storage.DeleteAllEntries();
+  ASSERT_EQ(1u, entries().count("foo2"));
+  EXPECT_EQ(nullptr, entries()["foo2"]->value);
+}
+
+TEST_P(StorageTestPopulated, DeleteAllEntriesPersistent) {
+  GetEntry("foo2")->flags = NT_PERSISTENT;
+
+  EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::ClearEntries()),
+                                        IsNull(), IsNull()));
+  EXPECT_CALL(notifier, NotifyEntry(_, _, _, NT_NOTIFY_DELETE | NT_NOTIFY_LOCAL,
+                                    UINT_MAX))
+      .Times(3);
+
+  storage.DeleteAllEntries();
+  ASSERT_EQ(1u, entries().count("foo2"));
+  EXPECT_NE(nullptr, entries()["foo2"]->value);
+}
+
+TEST_P(StorageTestPopulated, GetEntryInfoAll) {
+  auto info = storage.GetEntryInfo(0, "", 0u);
+  ASSERT_EQ(4u, info.size());
+}
+
+TEST_P(StorageTestPopulated, GetEntryInfoPrefix) {
+  auto info = storage.GetEntryInfo(0, "foo", 0u);
+  ASSERT_EQ(2u, info.size());
+  if (info[0].name == "foo") {
+    EXPECT_EQ("foo", info[0].name);
+    EXPECT_EQ(NT_BOOLEAN, info[0].type);
+    EXPECT_EQ("foo2", info[1].name);
+    EXPECT_EQ(NT_DOUBLE, info[1].type);
+  } else {
+    EXPECT_EQ("foo2", info[0].name);
+    EXPECT_EQ(NT_DOUBLE, info[0].type);
+    EXPECT_EQ("foo", info[1].name);
+    EXPECT_EQ(NT_BOOLEAN, info[1].type);
+  }
+}
+
+TEST_P(StorageTestPopulated, GetEntryInfoTypes) {
+  auto info = storage.GetEntryInfo(0, "", NT_DOUBLE);
+  ASSERT_EQ(2u, info.size());
+  EXPECT_EQ(NT_DOUBLE, info[0].type);
+  EXPECT_EQ(NT_DOUBLE, info[1].type);
+  if (info[0].name == "foo2") {
+    EXPECT_EQ("foo2", info[0].name);
+    EXPECT_EQ("bar", info[1].name);
+  } else {
+    EXPECT_EQ("bar", info[0].name);
+    EXPECT_EQ("foo2", info[1].name);
+  }
+}
+
+TEST_P(StorageTestPopulated, GetEntryInfoPrefixTypes) {
+  auto info = storage.GetEntryInfo(0, "bar", NT_BOOLEAN);
+  ASSERT_EQ(1u, info.size());
+  EXPECT_EQ("bar2", info[0].name);
+  EXPECT_EQ(NT_BOOLEAN, info[0].type);
+}
+
+TEST_P(StorageTestPersistent, SavePersistentEmpty) {
+  wpi::SmallString<256> buf;
+  wpi::raw_svector_ostream oss(buf);
+  storage.SavePersistent(oss, false);
+  ASSERT_EQ("[NetworkTables Storage 3.0]\n", oss.str());
+}
+
+TEST_P(StorageTestPersistent, SavePersistent) {
+  for (auto& i : entries()) i.getValue()->flags = NT_PERSISTENT;
+  wpi::SmallString<256> buf;
+  wpi::raw_svector_ostream oss(buf);
+  storage.SavePersistent(oss, false);
+  wpi::StringRef out = oss.str();
+  // std::fputs(out.c_str(), stderr);
+  wpi::StringRef line, rem = out;
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("[NetworkTables Storage 3.0]", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("boolean \"\\x00\\x03\\x05\\n\"=true", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("boolean \"\\x3D\"=true", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("boolean \"boolean/false\"=false", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("boolean \"boolean/true\"=true", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("array boolean \"booleanarr/empty\"=", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("array boolean \"booleanarr/one\"=true", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("array boolean \"booleanarr/two\"=true,false", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("double \"double/big\"=1.3e+08", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("double \"double/neg\"=-1.5", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("double \"double/zero\"=0", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("array double \"doublearr/empty\"=", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("array double \"doublearr/one\"=0.5", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("array double \"doublearr/two\"=0.5,-0.25", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("raw \"raw/empty\"=", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("raw \"raw/normal\"=aGVsbG8=", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("raw \"raw/special\"=AAMFCg==", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("string \"string/empty\"=\"\"", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("string \"string/normal\"=\"hello\"", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("string \"string/special\"=\"\\x00\\x03\\x05\\n\"", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("array string \"stringarr/empty\"=", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("array string \"stringarr/one\"=\"hello\"", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("array string \"stringarr/two\"=\"hello\",\"world\\n\"", line);
+  std::tie(line, rem) = rem.split('\n');
+  ASSERT_EQ("", line);
+}
+
+TEST_P(StorageTestEmpty, LoadPersistentBadHeader) {
+  MockLoadWarn warn;
+  auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
+
+  wpi::raw_mem_istream iss("");
+  EXPECT_CALL(
+      warn,
+      Warn(1, wpi::StringRef("header line mismatch, ignoring rest of file")));
+  EXPECT_FALSE(storage.LoadEntries(iss, "", true, warn_func));
+
+  wpi::raw_mem_istream iss2("[NetworkTables");
+  EXPECT_CALL(
+      warn,
+      Warn(1, wpi::StringRef("header line mismatch, ignoring rest of file")));
+
+  EXPECT_FALSE(storage.LoadEntries(iss2, "", true, warn_func));
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestEmpty, LoadPersistentCommentHeader) {
+  MockLoadWarn warn;
+  auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
+
+  wpi::raw_mem_istream iss(
+      "\n; comment\n# comment\n[NetworkTables Storage 3.0]\n");
+  EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestEmpty, LoadPersistentEmptyName) {
+  MockLoadWarn warn;
+  auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
+
+  wpi::raw_mem_istream iss("[NetworkTables Storage 3.0]\nboolean \"\"=true\n");
+  EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestEmpty, LoadPersistentAssign) {
+  MockLoadWarn warn;
+  auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
+
+  auto value = Value::MakeBoolean(true);
+
+  // id assigned if server
+  EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::EntryAssign(
+                                            "foo", GetParam() ? 0 : 0xffff, 1,
+                                            value, NT_PERSISTENT)),
+                                        IsNull(), IsNull()));
+  EXPECT_CALL(notifier, NotifyEntry(0, StringRef("foo"),
+                                    ValueEq(Value::MakeBoolean(true)),
+                                    NT_NOTIFY_NEW | NT_NOTIFY_LOCAL, UINT_MAX));
+
+  wpi::raw_mem_istream iss(
+      "[NetworkTables Storage 3.0]\nboolean \"foo\"=true\n");
+  EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
+  auto entry = GetEntry("foo");
+  EXPECT_EQ(*value, *entry->value);
+  EXPECT_EQ(NT_PERSISTENT, entry->flags);
+}
+
+TEST_P(StorageTestPopulated, LoadPersistentUpdateFlags) {
+  MockLoadWarn warn;
+  auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
+
+  // client shouldn't send an update as id not assigned yet
+  if (GetParam()) {
+    // id assigned as this is server
+    EXPECT_CALL(dispatcher,
+                QueueOutgoing(MessageEq(Message::FlagsUpdate(1, NT_PERSISTENT)),
+                              IsNull(), IsNull()));
+  }
+  EXPECT_CALL(notifier,
+              NotifyEntry(1, StringRef("foo2"), ValueEq(Value::MakeDouble(0)),
+                          NT_NOTIFY_FLAGS | NT_NOTIFY_LOCAL, UINT_MAX));
+
+  wpi::raw_mem_istream iss(
+      "[NetworkTables Storage 3.0]\ndouble \"foo2\"=0.0\n");
+  EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
+  auto entry = GetEntry("foo2");
+  EXPECT_EQ(*Value::MakeDouble(0.0), *entry->value);
+  EXPECT_EQ(NT_PERSISTENT, entry->flags);
+}
+
+TEST_P(StorageTestPopulated, LoadPersistentUpdateValue) {
+  MockLoadWarn warn;
+  auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
+
+  GetEntry("foo2")->flags = NT_PERSISTENT;
+
+  auto value = Value::MakeDouble(1.0);
+
+  // client shouldn't send an update as id not assigned yet
+  if (GetParam()) {
+    // id assigned as this is the server; seq_num incremented
+    EXPECT_CALL(dispatcher,
+                QueueOutgoing(MessageEq(Message::EntryUpdate(1, 2, value)),
+                              IsNull(), IsNull()));
+  }
+  EXPECT_CALL(notifier,
+              NotifyEntry(1, StringRef("foo2"), ValueEq(Value::MakeDouble(1)),
+                          NT_NOTIFY_UPDATE | NT_NOTIFY_LOCAL, UINT_MAX));
+
+  wpi::raw_mem_istream iss(
+      "[NetworkTables Storage 3.0]\ndouble \"foo2\"=1.0\n");
+  EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
+  auto entry = GetEntry("foo2");
+  EXPECT_EQ(*value, *entry->value);
+  EXPECT_EQ(NT_PERSISTENT, entry->flags);
+
+  if (!GetParam()) {
+    // seq_num should still be incremented
+    EXPECT_EQ(2u, GetEntry("foo2")->seq_num.value());
+  }
+}
+
+TEST_P(StorageTestPopulated, LoadPersistentUpdateValueFlags) {
+  MockLoadWarn warn;
+  auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
+
+  auto value = Value::MakeDouble(1.0);
+
+  // client shouldn't send an update as id not assigned yet
+  if (GetParam()) {
+    // id assigned as this is the server; seq_num incremented
+    EXPECT_CALL(dispatcher,
+                QueueOutgoing(MessageEq(Message::EntryUpdate(1, 2, value)),
+                              IsNull(), IsNull()));
+    EXPECT_CALL(dispatcher,
+                QueueOutgoing(MessageEq(Message::FlagsUpdate(1, NT_PERSISTENT)),
+                              IsNull(), IsNull()));
+  }
+  EXPECT_CALL(notifier,
+              NotifyEntry(1, StringRef("foo2"), ValueEq(Value::MakeDouble(1)),
+                          NT_NOTIFY_FLAGS | NT_NOTIFY_UPDATE | NT_NOTIFY_LOCAL,
+                          UINT_MAX));
+
+  wpi::raw_mem_istream iss(
+      "[NetworkTables Storage 3.0]\ndouble \"foo2\"=1.0\n");
+  EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
+  auto entry = GetEntry("foo2");
+  EXPECT_EQ(*value, *entry->value);
+  EXPECT_EQ(NT_PERSISTENT, entry->flags);
+
+  if (!GetParam()) {
+    // seq_num should still be incremented
+    EXPECT_EQ(2u, GetEntry("foo2")->seq_num.value());
+  }
+}
+
+TEST_P(StorageTestEmpty, LoadPersistent) {
+  MockLoadWarn warn;
+  auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
+
+  std::string in = "[NetworkTables Storage 3.0]\n";
+  in += "boolean \"\\x00\\x03\\x05\\n\"=true\n";
+  in += "boolean \"\\x3D\"=true\n";
+  in += "boolean \"boolean/false\"=false\n";
+  in += "boolean \"boolean/true\"=true\n";
+  in += "array boolean \"booleanarr/empty\"=\n";
+  in += "array boolean \"booleanarr/one\"=true\n";
+  in += "array boolean \"booleanarr/two\"=true,false\n";
+  in += "double \"double/big\"=1.3e+08\n";
+  in += "double \"double/neg\"=-1.5\n";
+  in += "double \"double/zero\"=0\n";
+  in += "array double \"doublearr/empty\"=\n";
+  in += "array double \"doublearr/one\"=0.5\n";
+  in += "array double \"doublearr/two\"=0.5,-0.25\n";
+  in += "raw \"raw/empty\"=\n";
+  in += "raw \"raw/normal\"=aGVsbG8=\n";
+  in += "raw \"raw/special\"=AAMFCg==\n";
+  in += "string \"string/empty\"=\"\"\n";
+  in += "string \"string/normal\"=\"hello\"\n";
+  in += "string \"string/special\"=\"\\x00\\x03\\x05\\n\"\n";
+  in += "array string \"stringarr/empty\"=\n";
+  in += "array string \"stringarr/one\"=\"hello\"\n";
+  in += "array string \"stringarr/two\"=\"hello\",\"world\\n\"\n";
+
+  EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(22);
+  EXPECT_CALL(notifier,
+              NotifyEntry(_, _, _, NT_NOTIFY_NEW | NT_NOTIFY_LOCAL, UINT_MAX))
+      .Times(22);
+
+  wpi::raw_mem_istream iss(in);
+  EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
+  ASSERT_EQ(22u, entries().size());
+
+  EXPECT_EQ(*Value::MakeBoolean(true), *storage.GetEntryValue("boolean/true"));
+  EXPECT_EQ(*Value::MakeBoolean(false),
+            *storage.GetEntryValue("boolean/false"));
+  EXPECT_EQ(*Value::MakeDouble(-1.5), *storage.GetEntryValue("double/neg"));
+  EXPECT_EQ(*Value::MakeDouble(0.0), *storage.GetEntryValue("double/zero"));
+  EXPECT_EQ(*Value::MakeDouble(1.3e8), *storage.GetEntryValue("double/big"));
+  EXPECT_EQ(*Value::MakeString(""), *storage.GetEntryValue("string/empty"));
+  EXPECT_EQ(*Value::MakeString("hello"),
+            *storage.GetEntryValue("string/normal"));
+  EXPECT_EQ(*Value::MakeString(StringRef("\0\3\5\n", 4)),
+            *storage.GetEntryValue("string/special"));
+  EXPECT_EQ(*Value::MakeRaw(""), *storage.GetEntryValue("raw/empty"));
+  EXPECT_EQ(*Value::MakeRaw("hello"), *storage.GetEntryValue("raw/normal"));
+  EXPECT_EQ(*Value::MakeRaw(StringRef("\0\3\5\n", 4)),
+            *storage.GetEntryValue("raw/special"));
+  EXPECT_EQ(*Value::MakeBooleanArray(std::vector<int>{}),
+            *storage.GetEntryValue("booleanarr/empty"));
+  EXPECT_EQ(*Value::MakeBooleanArray(std::vector<int>{1}),
+            *storage.GetEntryValue("booleanarr/one"));
+  EXPECT_EQ(*Value::MakeBooleanArray(std::vector<int>{1, 0}),
+            *storage.GetEntryValue("booleanarr/two"));
+  EXPECT_EQ(*Value::MakeDoubleArray(std::vector<double>{}),
+            *storage.GetEntryValue("doublearr/empty"));
+  EXPECT_EQ(*Value::MakeDoubleArray(std::vector<double>{0.5}),
+            *storage.GetEntryValue("doublearr/one"));
+  EXPECT_EQ(*Value::MakeDoubleArray(std::vector<double>{0.5, -0.25}),
+            *storage.GetEntryValue("doublearr/two"));
+  EXPECT_EQ(*Value::MakeStringArray(std::vector<std::string>{}),
+            *storage.GetEntryValue("stringarr/empty"));
+  EXPECT_EQ(*Value::MakeStringArray(std::vector<std::string>{"hello"}),
+            *storage.GetEntryValue("stringarr/one"));
+  EXPECT_EQ(
+      *Value::MakeStringArray(std::vector<std::string>{"hello", "world\n"}),
+      *storage.GetEntryValue("stringarr/two"));
+  EXPECT_EQ(*Value::MakeBoolean(true),
+            *storage.GetEntryValue(StringRef("\0\3\5\n", 4)));
+  EXPECT_EQ(*Value::MakeBoolean(true), *storage.GetEntryValue("="));
+}
+
+TEST_P(StorageTestEmpty, LoadPersistentWarn) {
+  MockLoadWarn warn;
+  auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
+
+  wpi::raw_mem_istream iss(
+      "[NetworkTables Storage 3.0]\nboolean \"foo\"=foo\n");
+  EXPECT_CALL(
+      warn, Warn(2, wpi::StringRef(
+                        "unrecognized boolean value, not 'true' or 'false'")));
+  EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
+
+  EXPECT_TRUE(entries().empty());
+  EXPECT_TRUE(idmap().empty());
+}
+
+TEST_P(StorageTestEmpty, ProcessIncomingEntryAssign) {
+  auto conn = std::make_shared<MockNetworkConnection>();
+  auto value = Value::MakeDouble(1.0);
+  if (GetParam()) {
+    // id assign message reply generated on the server; sent to everyone
+    EXPECT_CALL(
+        dispatcher,
+        QueueOutgoing(MessageEq(Message::EntryAssign("foo", 0, 0, value, 0)),
+                      IsNull(), IsNull()));
+  }
+  EXPECT_CALL(notifier, NotifyEntry(0, StringRef("foo"), ValueEq(value),
+                                    NT_NOTIFY_NEW, UINT_MAX));
+
+  storage.ProcessIncoming(
+      Message::EntryAssign("foo", GetParam() ? 0xffff : 0, 0, value, 0),
+      conn.get(), conn);
+}
+
+TEST_P(StorageTestPopulateOne, ProcessIncomingEntryAssign) {
+  auto conn = std::make_shared<MockNetworkConnection>();
+  auto value = Value::MakeDouble(1.0);
+  EXPECT_CALL(*conn, proto_rev()).WillRepeatedly(Return(0x0300u));
+  if (GetParam()) {
+    // server broadcasts new value to all *other* connections
+    EXPECT_CALL(
+        dispatcher,
+        QueueOutgoing(MessageEq(Message::EntryAssign("foo", 0, 1, value, 0)),
+                      IsNull(), conn.get()));
+  }
+  EXPECT_CALL(notifier, NotifyEntry(0, StringRef("foo"), ValueEq(value),
+                                    NT_NOTIFY_UPDATE, UINT_MAX));
+
+  storage.ProcessIncoming(Message::EntryAssign("foo", 0, 1, value, 0),
+                          conn.get(), conn);
+}
+
+TEST_P(StorageTestPopulateOne, ProcessIncomingEntryAssignIgnore) {
+  auto conn = std::make_shared<MockNetworkConnection>();
+  auto value = Value::MakeDouble(1.0);
+  storage.ProcessIncoming(Message::EntryAssign("foo", 0xffff, 1, value, 0),
+                          conn.get(), conn);
+}
+
+TEST_P(StorageTestPopulateOne, ProcessIncomingEntryAssignWithFlags) {
+  auto conn = std::make_shared<MockNetworkConnection>();
+  auto value = Value::MakeDouble(1.0);
+  EXPECT_CALL(*conn, proto_rev()).WillRepeatedly(Return(0x0300u));
+  if (GetParam()) {
+    // server broadcasts new value/flags to all *other* connections
+    EXPECT_CALL(
+        dispatcher,
+        QueueOutgoing(MessageEq(Message::EntryAssign("foo", 0, 1, value, 0x2)),
+                      IsNull(), conn.get()));
+    EXPECT_CALL(notifier,
+                NotifyEntry(0, StringRef("foo"), ValueEq(value),
+                            NT_NOTIFY_UPDATE | NT_NOTIFY_FLAGS, UINT_MAX));
+  } else {
+    // client forces flags back when an assign message is received for an
+    // existing entry with different flags
+    EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::FlagsUpdate(0, 0)),
+                                          IsNull(), IsNull()));
+    EXPECT_CALL(notifier, NotifyEntry(0, StringRef("foo"), ValueEq(value),
+                                      NT_NOTIFY_UPDATE, UINT_MAX));
+  }
+
+  storage.ProcessIncoming(Message::EntryAssign("foo", 0, 1, value, 0x2),
+                          conn.get(), conn);
+}
+
+TEST_P(StorageTestPopulateOne, DeleteCheckHandle) {
+  EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
+  EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
+  auto handle = storage.GetEntry("foo");
+  storage.DeleteEntry("foo");
+  storage.SetEntryTypeValue("foo", Value::MakeBoolean(true));
+  ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
+  ::testing::Mock::VerifyAndClearExpectations(&notifier);
+
+  auto handle2 = storage.GetEntry("foo");
+  ASSERT_EQ(handle, handle2);
+}
+
+TEST_P(StorageTestPopulateOne, DeletedEntryFlags) {
+  EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
+  EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
+  auto handle = storage.GetEntry("foo");
+  storage.SetEntryFlags("foo", 2);
+  storage.DeleteEntry("foo");
+  ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
+  ::testing::Mock::VerifyAndClearExpectations(&notifier);
+
+  EXPECT_EQ(storage.GetEntryFlags("foo"), 0u);
+  EXPECT_EQ(storage.GetEntryFlags(handle), 0u);
+  storage.SetEntryFlags("foo", 4);
+  storage.SetEntryFlags(handle, 4);
+  EXPECT_EQ(storage.GetEntryFlags("foo"), 0u);
+  EXPECT_EQ(storage.GetEntryFlags(handle), 0u);
+}
+
+TEST_P(StorageTestPopulateOne, DeletedDeleteAllEntries) {
+  EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
+  EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
+  storage.DeleteEntry("foo");
+  ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
+  ::testing::Mock::VerifyAndClearExpectations(&notifier);
+
+  EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::ClearEntries()),
+                                        IsNull(), IsNull()));
+  storage.DeleteAllEntries();
+}
+
+TEST_P(StorageTestPopulateOne, DeletedGetEntries) {
+  EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
+  EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
+  storage.DeleteEntry("foo");
+  ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
+  ::testing::Mock::VerifyAndClearExpectations(&notifier);
+
+  EXPECT_TRUE(storage.GetEntries("", 0).empty());
+}
+
+INSTANTIATE_TEST_CASE_P(StorageTestsEmpty, StorageTestEmpty,
+                        ::testing::Bool(), );
+INSTANTIATE_TEST_CASE_P(StorageTestsPopulateOne, StorageTestPopulateOne,
+                        ::testing::Bool(), );
+INSTANTIATE_TEST_CASE_P(StorageTestsPopulated, StorageTestPopulated,
+                        ::testing::Bool(), );
+INSTANTIATE_TEST_CASE_P(StorageTestsPersistent, StorageTestPersistent,
+                        ::testing::Bool(), );
+
+}  // namespace nt
diff --git a/ntcore/src/test/native/cpp/StorageTest.h b/ntcore/src/test/native/cpp/StorageTest.h
new file mode 100644
index 0000000..1bb8a8c
--- /dev/null
+++ b/ntcore/src/test/native/cpp/StorageTest.h
@@ -0,0 +1,47 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_STORAGETEST_H_
+#define NTCORE_STORAGETEST_H_
+
+#include <functional>
+#include <memory>
+#include <vector>
+
+#include "Log.h"
+#include "MockDispatcher.h"
+#include "MockEntryNotifier.h"
+#include "MockRpcServer.h"
+#include "Storage.h"
+
+namespace nt {
+
+class StorageTest {
+ public:
+  StorageTest() : storage(notifier, rpc_server, logger), tmp_entry("foobar") {}
+
+  Storage::EntriesMap& entries() { return storage.m_entries; }
+  Storage::IdMap& idmap() { return storage.m_idmap; }
+
+  Storage::Entry* GetEntry(StringRef name) {
+    auto i = storage.m_entries.find(name);
+    return i == storage.m_entries.end() ? &tmp_entry : i->getValue();
+  }
+
+  void HookOutgoing(bool server) { storage.SetDispatcher(&dispatcher, server); }
+
+  wpi::Logger logger;
+  ::testing::StrictMock<MockEntryNotifier> notifier;
+  ::testing::StrictMock<MockRpcServer> rpc_server;
+  ::testing::StrictMock<MockDispatcher> dispatcher;
+  Storage storage;
+  Storage::Entry tmp_entry;
+};
+
+}  // namespace nt
+
+#endif  // NTCORE_STORAGETEST_H_
diff --git a/ntcore/src/test/native/cpp/TestPrinters.cpp b/ntcore/src/test/native/cpp/TestPrinters.cpp
new file mode 100644
index 0000000..3368b8c
--- /dev/null
+++ b/ntcore/src/test/native/cpp/TestPrinters.cpp
@@ -0,0 +1,156 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "TestPrinters.h"
+
+#include "Handle.h"
+#include "Message.h"
+#include "networktables/NetworkTableValue.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+void PrintTo(const EntryNotification& event, std::ostream* os) {
+  *os << "EntryNotification{listener=";
+  PrintTo(Handle{event.listener}, os);
+  *os << ", entry=";
+  PrintTo(Handle{event.entry}, os);
+  *os << ", name=\"" << event.name << "\", flags=" << event.flags << ", value=";
+  PrintTo(event.value, os);
+  *os << '}';
+}
+
+void PrintTo(const Handle& handle, std::ostream* os) {
+  *os << "Handle{";
+  switch (handle.GetType()) {
+    case Handle::kConnectionListener:
+      *os << "kConnectionListener";
+      break;
+    case Handle::kConnectionListenerPoller:
+      *os << "kConnectionListenerPoller";
+      break;
+    case Handle::kEntry:
+      *os << "kEntry";
+      break;
+    case Handle::kEntryListener:
+      *os << "kEntryListener";
+      break;
+    case Handle::kEntryListenerPoller:
+      *os << "kEntryListenerPoller";
+      break;
+    case Handle::kInstance:
+      *os << "kInstance";
+      break;
+    case Handle::kLogger:
+      *os << "kLogger";
+      break;
+    case Handle::kLoggerPoller:
+      *os << "kLoggerPoller";
+      break;
+    case Handle::kRpcCall:
+      *os << "kRpcCall";
+      break;
+    case Handle::kRpcCallPoller:
+      *os << "kRpcCallPoller";
+      break;
+    default:
+      *os << "UNKNOWN";
+      break;
+  }
+  *os << ", " << handle.GetInst() << ", " << handle.GetIndex() << '}';
+}
+
+void PrintTo(const Message& msg, std::ostream* os) {
+  *os << "Message{";
+  switch (msg.type()) {
+    case Message::kKeepAlive:
+      *os << "kKeepAlive";
+      break;
+    case Message::kClientHello:
+      *os << "kClientHello";
+      break;
+    case Message::kProtoUnsup:
+      *os << "kProtoUnsup";
+      break;
+    case Message::kServerHelloDone:
+      *os << "kServerHelloDone";
+      break;
+    case Message::kServerHello:
+      *os << "kServerHello";
+      break;
+    case Message::kClientHelloDone:
+      *os << "kClientHelloDone";
+      break;
+    case Message::kEntryAssign:
+      *os << "kEntryAssign";
+      break;
+    case Message::kEntryUpdate:
+      *os << "kEntryUpdate";
+      break;
+    case Message::kFlagsUpdate:
+      *os << "kFlagsUpdate";
+      break;
+    case Message::kEntryDelete:
+      *os << "kEntryDelete";
+      break;
+    case Message::kClearEntries:
+      *os << "kClearEntries";
+      break;
+    case Message::kExecuteRpc:
+      *os << "kExecuteRpc";
+      break;
+    case Message::kRpcResponse:
+      *os << "kRpcResponse";
+      break;
+    default:
+      *os << "UNKNOWN";
+      break;
+  }
+  *os << ": str=\"" << msg.str() << "\", id=" << msg.id()
+      << ", flags=" << msg.flags() << ", seq_num_uid=" << msg.seq_num_uid()
+      << ", value=";
+  PrintTo(msg.value(), os);
+  *os << '}';
+}
+
+void PrintTo(const Value& value, std::ostream* os) {
+  *os << "Value{";
+  switch (value.type()) {
+    case NT_UNASSIGNED:
+      break;
+    case NT_BOOLEAN:
+      *os << (value.GetBoolean() ? "true" : "false");
+      break;
+    case NT_DOUBLE:
+      *os << value.GetDouble();
+      break;
+    case NT_STRING:
+      *os << '"' << value.GetString().str() << '"';
+      break;
+    case NT_RAW:
+      *os << ::testing::PrintToString(value.GetRaw());
+      break;
+    case NT_BOOLEAN_ARRAY:
+      *os << ::testing::PrintToString(value.GetBooleanArray());
+      break;
+    case NT_DOUBLE_ARRAY:
+      *os << ::testing::PrintToString(value.GetDoubleArray());
+      break;
+    case NT_STRING_ARRAY:
+      *os << ::testing::PrintToString(value.GetStringArray());
+      break;
+    case NT_RPC:
+      *os << ::testing::PrintToString(value.GetRpc());
+      break;
+    default:
+      *os << "UNKNOWN TYPE " << value.type();
+      break;
+  }
+  *os << '}';
+}
+
+}  // namespace nt
diff --git a/ntcore/src/test/native/cpp/TestPrinters.h b/ntcore/src/test/native/cpp/TestPrinters.h
new file mode 100644
index 0000000..2976c1c
--- /dev/null
+++ b/ntcore/src/test/native/cpp/TestPrinters.h
@@ -0,0 +1,54 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_TESTPRINTERS_H_
+#define NTCORE_TESTPRINTERS_H_
+
+#include <memory>
+#include <ostream>
+
+#include <wpi/StringRef.h>
+
+#include "gtest/gtest.h"
+
+namespace wpi {
+
+inline void PrintTo(StringRef str, ::std::ostream* os) {
+  ::testing::internal::PrintStringTo(str.str(), os);
+}
+
+}  // namespace wpi
+
+namespace nt {
+
+class EntryNotification;
+class Handle;
+class Message;
+class Value;
+
+void PrintTo(const EntryNotification& event, std::ostream* os);
+void PrintTo(const Handle& handle, std::ostream* os);
+
+void PrintTo(const Message& msg, std::ostream* os);
+
+inline void PrintTo(std::shared_ptr<Message> msg, std::ostream* os) {
+  *os << "shared_ptr{";
+  if (msg) PrintTo(*msg, os);
+  *os << '}';
+}
+
+void PrintTo(const Value& value, std::ostream* os);
+
+inline void PrintTo(std::shared_ptr<Value> value, std::ostream* os) {
+  *os << "shared_ptr{";
+  if (value) PrintTo(*value, os);
+  *os << '}';
+}
+
+}  // namespace nt
+
+#endif  // NTCORE_TESTPRINTERS_H_
diff --git a/ntcore/src/test/native/cpp/ValueMatcher.cpp b/ntcore/src/test/native/cpp/ValueMatcher.cpp
new file mode 100644
index 0000000..45b09ed
--- /dev/null
+++ b/ntcore/src/test/native/cpp/ValueMatcher.cpp
@@ -0,0 +1,33 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "ValueMatcher.h"
+
+#include "TestPrinters.h"
+
+namespace nt {
+
+bool ValueMatcher::MatchAndExplain(
+    std::shared_ptr<Value> val,
+    ::testing::MatchResultListener* listener) const {
+  if ((!val && goodval) || (val && !goodval) ||
+      (val && goodval && *val != *goodval)) {
+    return false;
+  }
+  return true;
+}
+
+void ValueMatcher::DescribeTo(::std::ostream* os) const {
+  PrintTo(goodval, os);
+}
+
+void ValueMatcher::DescribeNegationTo(::std::ostream* os) const {
+  *os << "is not equal to ";
+  PrintTo(goodval, os);
+}
+
+}  // namespace nt
diff --git a/ntcore/src/test/native/cpp/ValueMatcher.h b/ntcore/src/test/native/cpp/ValueMatcher.h
new file mode 100644
index 0000000..5c417d7
--- /dev/null
+++ b/ntcore/src/test/native/cpp/ValueMatcher.h
@@ -0,0 +1,40 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2017-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#ifndef NTCORE_VALUEMATCHER_H_
+#define NTCORE_VALUEMATCHER_H_
+
+#include <memory>
+#include <ostream>
+
+#include "gmock/gmock.h"
+#include "networktables/NetworkTableValue.h"
+
+namespace nt {
+
+class ValueMatcher
+    : public ::testing::MatcherInterface<std::shared_ptr<Value>> {
+ public:
+  explicit ValueMatcher(std::shared_ptr<Value> goodval_) : goodval(goodval_) {}
+
+  bool MatchAndExplain(std::shared_ptr<Value> msg,
+                       ::testing::MatchResultListener* listener) const override;
+  void DescribeTo(::std::ostream* os) const override;
+  void DescribeNegationTo(::std::ostream* os) const override;
+
+ private:
+  std::shared_ptr<Value> goodval;
+};
+
+inline ::testing::Matcher<std::shared_ptr<Value>> ValueEq(
+    std::shared_ptr<Value> goodval) {
+  return ::testing::MakeMatcher(new ValueMatcher(goodval));
+}
+
+}  // namespace nt
+
+#endif  // NTCORE_VALUEMATCHER_H_
diff --git a/ntcore/src/test/native/cpp/ValueTest.cpp b/ntcore/src/test/native/cpp/ValueTest.cpp
new file mode 100644
index 0000000..818cdac
--- /dev/null
+++ b/ntcore/src/test/native/cpp/ValueTest.cpp
@@ -0,0 +1,365 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include "TestPrinters.h"
+#include "Value_internal.h"
+#include "gtest/gtest.h"
+#include "networktables/NetworkTableValue.h"
+
+namespace nt {
+
+class ValueTest : public ::testing::Test {};
+
+typedef ValueTest ValueDeathTest;
+
+TEST_F(ValueTest, ConstructEmpty) {
+  Value v;
+  ASSERT_EQ(NT_UNASSIGNED, v.type());
+}
+
+TEST_F(ValueTest, Boolean) {
+  auto v = Value::MakeBoolean(false);
+  ASSERT_EQ(NT_BOOLEAN, v->type());
+  ASSERT_FALSE(v->GetBoolean());
+  NT_Value cv;
+  NT_InitValue(&cv);
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_BOOLEAN, cv.type);
+  ASSERT_EQ(0, cv.data.v_boolean);
+
+  v = Value::MakeBoolean(true);
+  ASSERT_EQ(NT_BOOLEAN, v->type());
+  ASSERT_TRUE(v->GetBoolean());
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_BOOLEAN, cv.type);
+  ASSERT_EQ(1, cv.data.v_boolean);
+
+  NT_DisposeValue(&cv);
+}
+
+TEST_F(ValueTest, Double) {
+  auto v = Value::MakeDouble(0.5);
+  ASSERT_EQ(NT_DOUBLE, v->type());
+  ASSERT_EQ(0.5, v->GetDouble());
+  NT_Value cv;
+  NT_InitValue(&cv);
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_DOUBLE, cv.type);
+  ASSERT_EQ(0.5, cv.data.v_double);
+
+  v = Value::MakeDouble(0.25);
+  ASSERT_EQ(NT_DOUBLE, v->type());
+  ASSERT_EQ(0.25, v->GetDouble());
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_DOUBLE, cv.type);
+  ASSERT_EQ(0.25, cv.data.v_double);
+
+  NT_DisposeValue(&cv);
+}
+
+TEST_F(ValueTest, String) {
+  auto v = Value::MakeString("hello");
+  ASSERT_EQ(NT_STRING, v->type());
+  ASSERT_EQ("hello", v->GetString());
+  NT_Value cv;
+  NT_InitValue(&cv);
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_STRING, cv.type);
+  ASSERT_EQ(wpi::StringRef("hello"), cv.data.v_string.str);
+  ASSERT_EQ(5u, cv.data.v_string.len);
+
+  v = Value::MakeString("goodbye");
+  ASSERT_EQ(NT_STRING, v->type());
+  ASSERT_EQ("goodbye", v->GetString());
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_STRING, cv.type);
+  ASSERT_EQ(wpi::StringRef("goodbye"), cv.data.v_string.str);
+  ASSERT_EQ(7u, cv.data.v_string.len);
+
+  NT_DisposeValue(&cv);
+}
+
+TEST_F(ValueTest, Raw) {
+  auto v = Value::MakeRaw("hello");
+  ASSERT_EQ(NT_RAW, v->type());
+  ASSERT_EQ("hello", v->GetRaw());
+  NT_Value cv;
+  NT_InitValue(&cv);
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_RAW, cv.type);
+  ASSERT_EQ(wpi::StringRef("hello"), cv.data.v_string.str);
+  ASSERT_EQ(5u, cv.data.v_string.len);
+
+  v = Value::MakeRaw("goodbye");
+  ASSERT_EQ(NT_RAW, v->type());
+  ASSERT_EQ("goodbye", v->GetRaw());
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_RAW, cv.type);
+  ASSERT_EQ(wpi::StringRef("goodbye"), cv.data.v_string.str);
+  ASSERT_EQ(7u, cv.data.v_string.len);
+
+  NT_DisposeValue(&cv);
+}
+
+TEST_F(ValueTest, BooleanArray) {
+  std::vector<int> vec{1, 0, 1};
+  auto v = Value::MakeBooleanArray(vec);
+  ASSERT_EQ(NT_BOOLEAN_ARRAY, v->type());
+  ASSERT_EQ(wpi::ArrayRef<int>(vec), v->GetBooleanArray());
+  NT_Value cv;
+  NT_InitValue(&cv);
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_BOOLEAN_ARRAY, cv.type);
+  ASSERT_EQ(3u, cv.data.arr_boolean.size);
+  ASSERT_EQ(vec[0], cv.data.arr_boolean.arr[0]);
+  ASSERT_EQ(vec[1], cv.data.arr_boolean.arr[1]);
+  ASSERT_EQ(vec[2], cv.data.arr_boolean.arr[2]);
+
+  // assign with same size
+  vec = {0, 1, 0};
+  v = Value::MakeBooleanArray(vec);
+  ASSERT_EQ(NT_BOOLEAN_ARRAY, v->type());
+  ASSERT_EQ(wpi::ArrayRef<int>(vec), v->GetBooleanArray());
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_BOOLEAN_ARRAY, cv.type);
+  ASSERT_EQ(3u, cv.data.arr_boolean.size);
+  ASSERT_EQ(vec[0], cv.data.arr_boolean.arr[0]);
+  ASSERT_EQ(vec[1], cv.data.arr_boolean.arr[1]);
+  ASSERT_EQ(vec[2], cv.data.arr_boolean.arr[2]);
+
+  // assign with different size
+  vec = {1, 0};
+  v = Value::MakeBooleanArray(vec);
+  ASSERT_EQ(NT_BOOLEAN_ARRAY, v->type());
+  ASSERT_EQ(wpi::ArrayRef<int>(vec), v->GetBooleanArray());
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_BOOLEAN_ARRAY, cv.type);
+  ASSERT_EQ(2u, cv.data.arr_boolean.size);
+  ASSERT_EQ(vec[0], cv.data.arr_boolean.arr[0]);
+  ASSERT_EQ(vec[1], cv.data.arr_boolean.arr[1]);
+
+  NT_DisposeValue(&cv);
+}
+
+TEST_F(ValueTest, DoubleArray) {
+  std::vector<double> vec{0.5, 0.25, 0.5};
+  auto v = Value::MakeDoubleArray(vec);
+  ASSERT_EQ(NT_DOUBLE_ARRAY, v->type());
+  ASSERT_EQ(wpi::ArrayRef<double>(vec), v->GetDoubleArray());
+  NT_Value cv;
+  NT_InitValue(&cv);
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_DOUBLE_ARRAY, cv.type);
+  ASSERT_EQ(3u, cv.data.arr_double.size);
+  ASSERT_EQ(vec[0], cv.data.arr_double.arr[0]);
+  ASSERT_EQ(vec[1], cv.data.arr_double.arr[1]);
+  ASSERT_EQ(vec[2], cv.data.arr_double.arr[2]);
+
+  // assign with same size
+  vec = {0.25, 0.5, 0.25};
+  v = Value::MakeDoubleArray(vec);
+  ASSERT_EQ(NT_DOUBLE_ARRAY, v->type());
+  ASSERT_EQ(wpi::ArrayRef<double>(vec), v->GetDoubleArray());
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_DOUBLE_ARRAY, cv.type);
+  ASSERT_EQ(3u, cv.data.arr_double.size);
+  ASSERT_EQ(vec[0], cv.data.arr_double.arr[0]);
+  ASSERT_EQ(vec[1], cv.data.arr_double.arr[1]);
+  ASSERT_EQ(vec[2], cv.data.arr_double.arr[2]);
+
+  // assign with different size
+  vec = {0.5, 0.25};
+  v = Value::MakeDoubleArray(vec);
+  ASSERT_EQ(NT_DOUBLE_ARRAY, v->type());
+  ASSERT_EQ(wpi::ArrayRef<double>(vec), v->GetDoubleArray());
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_DOUBLE_ARRAY, cv.type);
+  ASSERT_EQ(2u, cv.data.arr_double.size);
+  ASSERT_EQ(vec[0], cv.data.arr_double.arr[0]);
+  ASSERT_EQ(vec[1], cv.data.arr_double.arr[1]);
+
+  NT_DisposeValue(&cv);
+}
+
+TEST_F(ValueTest, StringArray) {
+  std::vector<std::string> vec;
+  vec.push_back("hello");
+  vec.push_back("goodbye");
+  vec.push_back("string");
+  auto v = Value::MakeStringArray(std::move(vec));
+  ASSERT_EQ(NT_STRING_ARRAY, v->type());
+  ASSERT_EQ(3u, v->GetStringArray().size());
+  ASSERT_EQ(wpi::StringRef("hello"), v->GetStringArray()[0]);
+  ASSERT_EQ(wpi::StringRef("goodbye"), v->GetStringArray()[1]);
+  ASSERT_EQ(wpi::StringRef("string"), v->GetStringArray()[2]);
+  NT_Value cv;
+  NT_InitValue(&cv);
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_STRING_ARRAY, cv.type);
+  ASSERT_EQ(3u, cv.data.arr_string.size);
+  ASSERT_EQ(wpi::StringRef("hello"), cv.data.arr_string.arr[0].str);
+  ASSERT_EQ(wpi::StringRef("goodbye"), cv.data.arr_string.arr[1].str);
+  ASSERT_EQ(wpi::StringRef("string"), cv.data.arr_string.arr[2].str);
+
+  // assign with same size
+  vec.clear();
+  vec.push_back("s1");
+  vec.push_back("str2");
+  vec.push_back("string3");
+  v = Value::MakeStringArray(vec);
+  ASSERT_EQ(NT_STRING_ARRAY, v->type());
+  ASSERT_EQ(3u, v->GetStringArray().size());
+  ASSERT_EQ(wpi::StringRef("s1"), v->GetStringArray()[0]);
+  ASSERT_EQ(wpi::StringRef("str2"), v->GetStringArray()[1]);
+  ASSERT_EQ(wpi::StringRef("string3"), v->GetStringArray()[2]);
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_STRING_ARRAY, cv.type);
+  ASSERT_EQ(3u, cv.data.arr_string.size);
+  ASSERT_EQ(wpi::StringRef("s1"), cv.data.arr_string.arr[0].str);
+  ASSERT_EQ(wpi::StringRef("str2"), cv.data.arr_string.arr[1].str);
+  ASSERT_EQ(wpi::StringRef("string3"), cv.data.arr_string.arr[2].str);
+
+  // assign with different size
+  vec.clear();
+  vec.push_back("short");
+  vec.push_back("er");
+  v = Value::MakeStringArray(std::move(vec));
+  ASSERT_EQ(NT_STRING_ARRAY, v->type());
+  ASSERT_EQ(2u, v->GetStringArray().size());
+  ASSERT_EQ(wpi::StringRef("short"), v->GetStringArray()[0]);
+  ASSERT_EQ(wpi::StringRef("er"), v->GetStringArray()[1]);
+  ConvertToC(*v, &cv);
+  ASSERT_EQ(NT_STRING_ARRAY, cv.type);
+  ASSERT_EQ(2u, cv.data.arr_string.size);
+  ASSERT_EQ(wpi::StringRef("short"), cv.data.arr_string.arr[0].str);
+  ASSERT_EQ(wpi::StringRef("er"), cv.data.arr_string.arr[1].str);
+
+  NT_DisposeValue(&cv);
+}
+
+TEST_F(ValueDeathTest, GetAssertions) {
+  Value v;
+  ASSERT_DEATH((void)v.GetBoolean(), "type == NT_BOOLEAN");
+  ASSERT_DEATH((void)v.GetDouble(), "type == NT_DOUBLE");
+  ASSERT_DEATH((void)v.GetString(), "type == NT_STRING");
+  ASSERT_DEATH((void)v.GetRaw(), "type == NT_RAW");
+  ASSERT_DEATH((void)v.GetBooleanArray(), "type == NT_BOOLEAN_ARRAY");
+  ASSERT_DEATH((void)v.GetDoubleArray(), "type == NT_DOUBLE_ARRAY");
+  ASSERT_DEATH((void)v.GetStringArray(), "type == NT_STRING_ARRAY");
+}
+
+TEST_F(ValueTest, UnassignedComparison) {
+  Value v1, v2;
+  ASSERT_EQ(v1, v2);
+}
+
+TEST_F(ValueTest, MixedComparison) {
+  Value v1;
+  auto v2 = Value::MakeBoolean(true);
+  ASSERT_NE(v1, *v2);  // unassigned vs boolean
+  auto v3 = Value::MakeDouble(0.5);
+  ASSERT_NE(*v2, *v3);  // boolean vs double
+}
+
+TEST_F(ValueTest, BooleanComparison) {
+  auto v1 = Value::MakeBoolean(true);
+  auto v2 = Value::MakeBoolean(true);
+  ASSERT_EQ(*v1, *v2);
+  v2 = Value::MakeBoolean(false);
+  ASSERT_NE(*v1, *v2);
+}
+
+TEST_F(ValueTest, DoubleComparison) {
+  auto v1 = Value::MakeDouble(0.25);
+  auto v2 = Value::MakeDouble(0.25);
+  ASSERT_EQ(*v1, *v2);
+  v2 = Value::MakeDouble(0.5);
+  ASSERT_NE(*v1, *v2);
+}
+
+TEST_F(ValueTest, StringComparison) {
+  auto v1 = Value::MakeString("hello");
+  auto v2 = Value::MakeString("hello");
+  ASSERT_EQ(*v1, *v2);
+  v2 = Value::MakeString("world");  // different contents
+  ASSERT_NE(*v1, *v2);
+  v2 = Value::MakeString("goodbye");  // different size
+  ASSERT_NE(*v1, *v2);
+}
+
+TEST_F(ValueTest, BooleanArrayComparison) {
+  std::vector<int> vec{1, 0, 1};
+  auto v1 = Value::MakeBooleanArray(vec);
+  auto v2 = Value::MakeBooleanArray(vec);
+  ASSERT_EQ(*v1, *v2);
+
+  // different contents
+  vec = {1, 1, 1};
+  v2 = Value::MakeBooleanArray(vec);
+  ASSERT_NE(*v1, *v2);
+
+  // different size
+  vec = {1, 0};
+  v2 = Value::MakeBooleanArray(vec);
+  ASSERT_NE(*v1, *v2);
+}
+
+TEST_F(ValueTest, DoubleArrayComparison) {
+  std::vector<double> vec{0.5, 0.25, 0.5};
+  auto v1 = Value::MakeDoubleArray(vec);
+  auto v2 = Value::MakeDoubleArray(vec);
+  ASSERT_EQ(*v1, *v2);
+
+  // different contents
+  vec = {0.5, 0.5, 0.5};
+  v2 = Value::MakeDoubleArray(vec);
+  ASSERT_NE(*v1, *v2);
+
+  // different size
+  vec = {0.5, 0.25};
+  v2 = Value::MakeDoubleArray(vec);
+  ASSERT_NE(*v1, *v2);
+}
+
+TEST_F(ValueTest, StringArrayComparison) {
+  std::vector<std::string> vec;
+  vec.push_back("hello");
+  vec.push_back("goodbye");
+  vec.push_back("string");
+  auto v1 = Value::MakeStringArray(vec);
+  vec.clear();
+  vec.push_back("hello");
+  vec.push_back("goodbye");
+  vec.push_back("string");
+  auto v2 = Value::MakeStringArray(std::move(vec));
+  ASSERT_EQ(*v1, *v2);
+
+  // different contents
+  vec.clear();
+  vec.push_back("hello");
+  vec.push_back("goodby2");
+  vec.push_back("string");
+  v2 = Value::MakeStringArray(std::move(vec));
+  ASSERT_NE(*v1, *v2);
+
+  // different sized contents
+  vec.clear();
+  vec.push_back("hello");
+  vec.push_back("goodbye2");
+  vec.push_back("string");
+  v2 = Value::MakeStringArray(vec);
+  ASSERT_NE(*v1, *v2);
+
+  // different size
+  vec.clear();
+  vec.push_back("hello");
+  vec.push_back("goodbye");
+  v2 = Value::MakeStringArray(std::move(vec));
+  ASSERT_NE(*v1, *v2);
+}
+
+}  // namespace nt
diff --git a/ntcore/src/test/native/cpp/WireDecoderTest.cpp b/ntcore/src/test/native/cpp/WireDecoderTest.cpp
new file mode 100644
index 0000000..a13fa7a
--- /dev/null
+++ b/ntcore/src/test/native/cpp/WireDecoderTest.cpp
@@ -0,0 +1,643 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <stdint.h>
+
+#include <cfloat>
+#include <climits>
+#include <string>
+
+#include <wpi/StringRef.h>
+
+#include "TestPrinters.h"
+#include "WireDecoder.h"
+#include "gtest/gtest.h"
+
+namespace nt {
+
+class WireDecoderTest : public ::testing::Test {
+ protected:
+  WireDecoderTest() {
+    v_boolean = Value::MakeBoolean(true);
+    v_double = Value::MakeDouble(1.0);
+    v_string = Value::MakeString(wpi::StringRef("hello"));
+    v_raw = Value::MakeRaw(wpi::StringRef("hello"));
+    v_boolean_array = Value::MakeBooleanArray(std::vector<int>{0, 1, 0});
+    v_boolean_array_big = Value::MakeBooleanArray(std::vector<int>(255));
+    v_double_array = Value::MakeDoubleArray(std::vector<double>{0.5, 0.25});
+    v_double_array_big = Value::MakeDoubleArray(std::vector<double>(255));
+
+    std::vector<std::string> sa;
+    sa.push_back("hello");
+    sa.push_back("goodbye");
+    v_string_array = Value::MakeStringArray(std::move(sa));
+
+    sa.clear();
+    for (int i = 0; i < 255; ++i) sa.push_back("h");
+    v_string_array_big = Value::MakeStringArray(std::move(sa));
+
+    s_normal = std::string("hello");
+
+    s_long.clear();
+    s_long.append(127, '*');
+    s_long.push_back('x');
+
+    s_big2.clear();
+    s_big2.append(65534, '*');
+    s_big2.push_back('x');
+
+    s_big3.clear();
+    s_big3.append(65534, '*');
+    s_big3.append(3, 'x');
+  }
+
+  std::shared_ptr<Value> v_boolean, v_double, v_string, v_raw;
+  std::shared_ptr<Value> v_boolean_array, v_boolean_array_big;
+  std::shared_ptr<Value> v_double_array, v_double_array_big;
+  std::shared_ptr<Value> v_string_array, v_string_array_big;
+
+  std::string s_normal, s_long, s_big2, s_big3;
+};
+
+TEST_F(WireDecoderTest, Construct) {
+  wpi::raw_mem_istream is("", 0);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  EXPECT_EQ(nullptr, d.error());
+  EXPECT_EQ(0x0300u, d.proto_rev());
+}
+
+TEST_F(WireDecoderTest, SetProtoRev) {
+  wpi::raw_mem_istream is("", 0);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  d.set_proto_rev(0x0200u);
+  EXPECT_EQ(0x0200u, d.proto_rev());
+}
+
+TEST_F(WireDecoderTest, Read8) {
+  wpi::raw_mem_istream is("\x05\x01\x00", 3);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  unsigned int val;
+  ASSERT_TRUE(d.Read8(&val));
+  EXPECT_EQ(5u, val);
+  ASSERT_TRUE(d.Read8(&val));
+  EXPECT_EQ(1u, val);
+  ASSERT_TRUE(d.Read8(&val));
+  EXPECT_EQ(0u, val);
+  ASSERT_FALSE(d.Read8(&val));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, Read16) {
+  wpi::raw_mem_istream is("\x00\x05\x00\x01\x45\x67\x00\x00", 8);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  unsigned int val;
+  ASSERT_TRUE(d.Read16(&val));
+  EXPECT_EQ(5u, val);
+  ASSERT_TRUE(d.Read16(&val));
+  EXPECT_EQ(1u, val);
+  ASSERT_TRUE(d.Read16(&val));
+  EXPECT_EQ(0x4567u, val);
+  ASSERT_TRUE(d.Read16(&val));
+  EXPECT_EQ(0u, val);
+  ASSERT_FALSE(d.Read16(&val));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, Read32) {
+  wpi::raw_mem_istream is(
+      "\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\xab\xcd"
+      "\x12\x34\x56\x78\x00\x00\x00\x00",
+      20);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  uint32_t val;
+  ASSERT_TRUE(d.Read32(&val));
+  EXPECT_EQ(5ul, val);
+  ASSERT_TRUE(d.Read32(&val));
+  EXPECT_EQ(1ul, val);
+  ASSERT_TRUE(d.Read32(&val));
+  EXPECT_EQ(0xabcdul, val);
+  ASSERT_TRUE(d.Read32(&val));
+  EXPECT_EQ(0x12345678ul, val);
+  ASSERT_TRUE(d.Read32(&val));
+  EXPECT_EQ(0ul, val);
+  ASSERT_FALSE(d.Read32(&val));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadDouble) {
+  // values except min and max from
+  // http://www.binaryconvert.com/result_double.html
+  wpi::raw_mem_istream is(
+      "\x00\x00\x00\x00\x00\x00\x00\x00"
+      "\x41\x0c\x13\x80\x00\x00\x00\x00"
+      "\x7f\xf0\x00\x00\x00\x00\x00\x00"
+      "\x00\x10\x00\x00\x00\x00\x00\x00"
+      "\x7f\xef\xff\xff\xff\xff\xff\xff",
+      40);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  double val;
+  ASSERT_TRUE(d.ReadDouble(&val));
+  EXPECT_EQ(0.0, val);
+  ASSERT_TRUE(d.ReadDouble(&val));
+  EXPECT_EQ(2.3e5, val);
+  ASSERT_TRUE(d.ReadDouble(&val));
+  EXPECT_EQ(std::numeric_limits<double>::infinity(), val);
+  ASSERT_TRUE(d.ReadDouble(&val));
+  EXPECT_EQ(DBL_MIN, val);
+  ASSERT_TRUE(d.ReadDouble(&val));
+  EXPECT_EQ(DBL_MAX, val);
+  ASSERT_FALSE(d.ReadDouble(&val));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadUleb128) {
+  wpi::raw_mem_istream is("\x00\x7f\x80\x01\x80", 5);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  uint64_t val;
+  ASSERT_TRUE(d.ReadUleb128(&val));
+  EXPECT_EQ(0ul, val);
+  ASSERT_TRUE(d.ReadUleb128(&val));
+  EXPECT_EQ(0x7ful, val);
+  ASSERT_TRUE(d.ReadUleb128(&val));
+  EXPECT_EQ(0x80ul, val);
+  ASSERT_FALSE(d.ReadUleb128(&val));  // partial
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadType) {
+  wpi::raw_mem_istream is("\x00\x01\x02\x03\x10\x11\x12\x20", 8);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  NT_Type val;
+  ASSERT_TRUE(d.ReadType(&val));
+  EXPECT_EQ(NT_BOOLEAN, val);
+  ASSERT_TRUE(d.ReadType(&val));
+  EXPECT_EQ(NT_DOUBLE, val);
+  ASSERT_TRUE(d.ReadType(&val));
+  EXPECT_EQ(NT_STRING, val);
+  ASSERT_TRUE(d.ReadType(&val));
+  EXPECT_EQ(NT_RAW, val);
+  ASSERT_TRUE(d.ReadType(&val));
+  EXPECT_EQ(NT_BOOLEAN_ARRAY, val);
+  ASSERT_TRUE(d.ReadType(&val));
+  EXPECT_EQ(NT_DOUBLE_ARRAY, val);
+  ASSERT_TRUE(d.ReadType(&val));
+  EXPECT_EQ(NT_STRING_ARRAY, val);
+  ASSERT_TRUE(d.ReadType(&val));
+  EXPECT_EQ(NT_RPC, val);
+  ASSERT_FALSE(d.ReadType(&val));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadTypeError) {
+  wpi::raw_mem_istream is("\x30", 1);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  NT_Type val;
+  ASSERT_FALSE(d.ReadType(&val));
+  EXPECT_EQ(NT_UNASSIGNED, val);
+  ASSERT_NE(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, Reset) {
+  wpi::raw_mem_istream is("\x30", 1);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  NT_Type val;
+  ASSERT_FALSE(d.ReadType(&val));
+  EXPECT_EQ(NT_UNASSIGNED, val);
+  ASSERT_NE(nullptr, d.error());
+  d.Reset();
+  EXPECT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadBooleanValue2) {
+  wpi::raw_mem_istream is("\x01\x00", 2);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  auto val = d.ReadValue(NT_BOOLEAN);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_boolean, *val);
+
+  auto v_false = Value::MakeBoolean(false);
+  val = d.ReadValue(NT_BOOLEAN);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_false, *val);
+
+  ASSERT_FALSE(d.ReadValue(NT_BOOLEAN));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadDoubleValue2) {
+  wpi::raw_mem_istream is(
+      "\x3f\xf0\x00\x00\x00\x00\x00\x00"
+      "\x3f\xf0\x00\x00\x00\x00\x00\x00",
+      16);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  auto val = d.ReadValue(NT_DOUBLE);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_double, *val);
+
+  val = d.ReadValue(NT_DOUBLE);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_double, *val);
+
+  ASSERT_FALSE(d.ReadValue(NT_DOUBLE));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadStringValue2) {
+  wpi::raw_mem_istream is(
+      "\x00\x05hello\x00\x03"
+      "bye\x55",
+      13);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  auto val = d.ReadValue(NT_STRING);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_string, *val);
+
+  auto v_bye = Value::MakeString(wpi::StringRef("bye"));
+  val = d.ReadValue(NT_STRING);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_bye, *val);
+
+  unsigned int b;
+  ASSERT_TRUE(d.Read8(&b));
+  EXPECT_EQ(0x55u, b);
+
+  ASSERT_FALSE(d.ReadValue(NT_STRING));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadBooleanArrayValue2) {
+  wpi::raw_mem_istream is("\x03\x00\x01\x00\x02\x01\x00\xff", 8);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  auto val = d.ReadValue(NT_BOOLEAN_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_boolean_array, *val);
+
+  auto v_boolean_array2 = Value::MakeBooleanArray(std::vector<int>{1, 0});
+  val = d.ReadValue(NT_BOOLEAN_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_boolean_array2, *val);
+
+  ASSERT_FALSE(d.ReadValue(NT_BOOLEAN_ARRAY));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadBooleanArrayBigValue2) {
+  std::string s;
+  s.push_back('\xff');
+  s.append(255, '\x00');
+  wpi::raw_mem_istream is(s.data(), s.size());
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  auto val = d.ReadValue(NT_BOOLEAN_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_boolean_array_big, *val);
+
+  ASSERT_FALSE(d.ReadValue(NT_BOOLEAN_ARRAY));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadDoubleArrayValue2) {
+  wpi::raw_mem_istream is(
+      "\x02\x3f\xe0\x00\x00\x00\x00\x00\x00"
+      "\x3f\xd0\x00\x00\x00\x00\x00\x00\x55",
+      18);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  auto val = d.ReadValue(NT_DOUBLE_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_double_array, *val);
+
+  unsigned int b;
+  ASSERT_TRUE(d.Read8(&b));
+  EXPECT_EQ(0x55u, b);
+
+  ASSERT_FALSE(d.ReadValue(NT_DOUBLE_ARRAY));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadDoubleArrayBigValue2) {
+  std::string s;
+  s.push_back('\xff');
+  s.append(255 * 8, '\x00');
+  wpi::raw_mem_istream is(s.data(), s.size());
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  auto val = d.ReadValue(NT_DOUBLE_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_double_array_big, *val);
+
+  ASSERT_FALSE(d.ReadValue(NT_DOUBLE_ARRAY));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadStringArrayValue2) {
+  wpi::raw_mem_istream is("\x02\x00\x05hello\x00\x07goodbye\x55", 18);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  auto val = d.ReadValue(NT_STRING_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_string_array, *val);
+
+  unsigned int b;
+  ASSERT_TRUE(d.Read8(&b));
+  EXPECT_EQ(0x55u, b);
+
+  ASSERT_FALSE(d.ReadValue(NT_STRING_ARRAY));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadStringArrayBigValue2) {
+  std::string s;
+  s.push_back('\xff');
+  for (int i = 0; i < 255; ++i) s.append("\x00\x01h", 3);
+  wpi::raw_mem_istream is(s.data(), s.size());
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  auto val = d.ReadValue(NT_STRING_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_string_array_big, *val);
+
+  ASSERT_FALSE(d.ReadValue(NT_STRING_ARRAY));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadValueError2) {
+  wpi::raw_mem_istream is("", 0);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  ASSERT_FALSE(d.ReadValue(NT_UNASSIGNED));  // unassigned
+  ASSERT_NE(nullptr, d.error());
+
+  d.Reset();
+  ASSERT_FALSE(d.ReadValue(NT_RAW));  // not supported
+  ASSERT_NE(nullptr, d.error());
+
+  d.Reset();
+  ASSERT_FALSE(d.ReadValue(NT_RPC));  // not supported
+  ASSERT_NE(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadBooleanValue3) {
+  wpi::raw_mem_istream is("\x01\x00", 2);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  auto val = d.ReadValue(NT_BOOLEAN);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_boolean, *val);
+
+  auto v_false = Value::MakeBoolean(false);
+  val = d.ReadValue(NT_BOOLEAN);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_false, *val);
+
+  ASSERT_FALSE(d.ReadValue(NT_BOOLEAN));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadDoubleValue3) {
+  wpi::raw_mem_istream is(
+      "\x3f\xf0\x00\x00\x00\x00\x00\x00"
+      "\x3f\xf0\x00\x00\x00\x00\x00\x00",
+      16);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  auto val = d.ReadValue(NT_DOUBLE);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_double, *val);
+
+  val = d.ReadValue(NT_DOUBLE);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_double, *val);
+
+  ASSERT_FALSE(d.ReadValue(NT_DOUBLE));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadStringValue3) {
+  wpi::raw_mem_istream is(
+      "\x05hello\x03"
+      "bye\x55",
+      11);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  auto val = d.ReadValue(NT_STRING);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_string, *val);
+
+  auto v_bye = Value::MakeString(wpi::StringRef("bye"));
+  val = d.ReadValue(NT_STRING);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_bye, *val);
+
+  unsigned int b;
+  ASSERT_TRUE(d.Read8(&b));
+  EXPECT_EQ(0x55u, b);
+
+  ASSERT_FALSE(d.ReadValue(NT_STRING));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadRawValue3) {
+  wpi::raw_mem_istream is(
+      "\x05hello\x03"
+      "bye\x55",
+      11);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  auto val = d.ReadValue(NT_RAW);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_raw, *val);
+
+  auto v_bye = Value::MakeRaw(wpi::StringRef("bye"));
+  val = d.ReadValue(NT_RAW);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_bye, *val);
+
+  unsigned int b;
+  ASSERT_TRUE(d.Read8(&b));
+  EXPECT_EQ(0x55u, b);
+
+  ASSERT_FALSE(d.ReadValue(NT_RAW));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadBooleanArrayValue3) {
+  wpi::raw_mem_istream is("\x03\x00\x01\x00\x02\x01\x00\xff", 8);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  auto val = d.ReadValue(NT_BOOLEAN_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_boolean_array, *val);
+
+  auto v_boolean_array2 = Value::MakeBooleanArray(std::vector<int>{1, 0});
+  val = d.ReadValue(NT_BOOLEAN_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_boolean_array2, *val);
+
+  ASSERT_FALSE(d.ReadValue(NT_BOOLEAN_ARRAY));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadBooleanArrayBigValue3) {
+  std::string s;
+  s.push_back('\xff');
+  s.append(255, '\x00');
+  wpi::raw_mem_istream is(s.data(), s.size());
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  auto val = d.ReadValue(NT_BOOLEAN_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_boolean_array_big, *val);
+
+  ASSERT_FALSE(d.ReadValue(NT_BOOLEAN_ARRAY));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadDoubleArrayValue3) {
+  wpi::raw_mem_istream is(
+      "\x02\x3f\xe0\x00\x00\x00\x00\x00\x00"
+      "\x3f\xd0\x00\x00\x00\x00\x00\x00\x55",
+      18);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  auto val = d.ReadValue(NT_DOUBLE_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_double_array, *val);
+
+  unsigned int b;
+  ASSERT_TRUE(d.Read8(&b));
+  EXPECT_EQ(0x55u, b);
+
+  ASSERT_FALSE(d.ReadValue(NT_DOUBLE_ARRAY));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadDoubleArrayBigValue3) {
+  std::string s;
+  s.push_back('\xff');
+  s.append(255 * 8, '\x00');
+  wpi::raw_mem_istream is(s.data(), s.size());
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  auto val = d.ReadValue(NT_DOUBLE_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_double_array_big, *val);
+
+  ASSERT_FALSE(d.ReadValue(NT_DOUBLE_ARRAY));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadStringArrayValue3) {
+  wpi::raw_mem_istream is("\x02\x05hello\x07goodbye\x55", 16);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  auto val = d.ReadValue(NT_STRING_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_string_array, *val);
+
+  unsigned int b;
+  ASSERT_TRUE(d.Read8(&b));
+  EXPECT_EQ(0x55u, b);
+
+  ASSERT_FALSE(d.ReadValue(NT_STRING_ARRAY));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadStringArrayBigValue3) {
+  std::string s;
+  s.push_back('\xff');
+  for (int i = 0; i < 255; ++i) s.append("\x01h", 2);
+  wpi::raw_mem_istream is(s.data(), s.size());
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  auto val = d.ReadValue(NT_STRING_ARRAY);
+  ASSERT_TRUE(static_cast<bool>(val));
+  EXPECT_EQ(*v_string_array_big, *val);
+
+  ASSERT_FALSE(d.ReadValue(NT_STRING_ARRAY));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadValueError3) {
+  wpi::raw_mem_istream is("", 0);
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  ASSERT_FALSE(d.ReadValue(NT_UNASSIGNED));  // unassigned
+  ASSERT_NE(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadString2) {
+  std::string s;
+  s.append("\x00\x05", 2);
+  s += s_normal;
+  s.append("\x00\x80", 2);
+  s += s_long;
+  s.append("\xff\xff", 2);
+  s += s_big2;
+  s.push_back('\x55');
+  wpi::raw_mem_istream is(s.data(), s.size());
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0200u, logger);
+  std::string outs;
+  ASSERT_TRUE(d.ReadString(&outs));
+  EXPECT_EQ(s_normal, outs);
+  ASSERT_TRUE(d.ReadString(&outs));
+  EXPECT_EQ(s_long, outs);
+  ASSERT_TRUE(d.ReadString(&outs));
+  EXPECT_EQ(s_big2, outs);
+
+  unsigned int b;
+  ASSERT_TRUE(d.Read8(&b));
+  EXPECT_EQ(0x55u, b);
+
+  ASSERT_FALSE(d.ReadString(&outs));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+TEST_F(WireDecoderTest, ReadString3) {
+  std::string s;
+  s.push_back('\x05');
+  s += s_normal;
+  s.append("\x80\x01", 2);
+  s += s_long;
+  s.append("\x81\x80\x04", 3);
+  s += s_big3;
+  s.push_back('\x55');
+  wpi::raw_mem_istream is(s.data(), s.size());
+  wpi::Logger logger;
+  WireDecoder d(is, 0x0300u, logger);
+  std::string outs;
+  ASSERT_TRUE(d.ReadString(&outs));
+  EXPECT_EQ(s_normal, outs);
+  ASSERT_TRUE(d.ReadString(&outs));
+  EXPECT_EQ(s_long, outs);
+  ASSERT_TRUE(d.ReadString(&outs));
+  EXPECT_EQ(s_big3, outs);
+
+  unsigned int b;
+  ASSERT_TRUE(d.Read8(&b));
+  EXPECT_EQ(0x55u, b);
+
+  ASSERT_FALSE(d.ReadString(&outs));
+  ASSERT_EQ(nullptr, d.error());
+}
+
+}  // namespace nt
diff --git a/ntcore/src/test/native/cpp/WireEncoderTest.cpp b/ntcore/src/test/native/cpp/WireEncoderTest.cpp
new file mode 100644
index 0000000..664344f
--- /dev/null
+++ b/ntcore/src/test/native/cpp/WireEncoderTest.cpp
@@ -0,0 +1,493 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <cfloat>
+#include <climits>
+#include <string>
+
+#include <wpi/StringRef.h>
+
+#include "TestPrinters.h"
+#include "WireEncoder.h"
+#include "gtest/gtest.h"
+
+#define BUFSIZE 1024
+
+namespace nt {
+
+class WireEncoderTest : public ::testing::Test {
+ protected:
+  WireEncoderTest() {
+    v_empty = std::make_shared<Value>();
+    v_boolean = Value::MakeBoolean(true);
+    v_double = Value::MakeDouble(1.0);
+    v_string = Value::MakeString(wpi::StringRef("hello"));
+    v_raw = Value::MakeRaw(wpi::StringRef("hello"));
+    v_boolean_array = Value::MakeBooleanArray(std::vector<int>{0, 1, 0});
+    v_boolean_array_big = Value::MakeBooleanArray(std::vector<int>(256));
+    v_double_array = Value::MakeDoubleArray(std::vector<double>{0.5, 0.25});
+    v_double_array_big = Value::MakeDoubleArray(std::vector<double>(256));
+
+    std::vector<std::string> sa;
+    sa.push_back("hello");
+    sa.push_back("goodbye");
+    v_string_array = Value::MakeStringArray(std::move(sa));
+
+    sa.clear();
+    for (int i = 0; i < 256; ++i) sa.push_back("h");
+    v_string_array_big = Value::MakeStringArray(std::move(sa));
+
+    s_normal = "hello";
+
+    s_long.clear();
+    s_long.append(127, '*');
+    s_long.push_back('x');
+
+    s_big.clear();
+    s_big.append(65534, '*');
+    s_big.append(3, 'x');
+  }
+
+  std::shared_ptr<Value> v_empty;
+  std::shared_ptr<Value> v_boolean, v_double, v_string, v_raw;
+  std::shared_ptr<Value> v_boolean_array, v_boolean_array_big;
+  std::shared_ptr<Value> v_double_array, v_double_array_big;
+  std::shared_ptr<Value> v_string_array, v_string_array_big;
+
+  std::string s_normal, s_long, s_big;
+};
+
+TEST_F(WireEncoderTest, Construct) {
+  WireEncoder e(0x0300u);
+  EXPECT_EQ(0u, e.size());
+  EXPECT_EQ(nullptr, e.error());
+  EXPECT_EQ(0x0300u, e.proto_rev());
+}
+
+TEST_F(WireEncoderTest, SetProtoRev) {
+  WireEncoder e(0x0300u);
+  e.set_proto_rev(0x0200u);
+  EXPECT_EQ(0x0200u, e.proto_rev());
+}
+
+TEST_F(WireEncoderTest, Write8) {
+  size_t off = BUFSIZE - 1;
+  WireEncoder e(0x0300u);
+  for (size_t i = 0; i < off; ++i) e.Write8(0u);  // test across Reserve()
+  e.Write8(5u);
+  e.Write8(0x101u);  // should be truncated
+  e.Write8(0u);
+  ASSERT_EQ(3u, e.size() - off);
+  ASSERT_EQ(wpi::StringRef("\x05\x01\x00", 3),
+            wpi::StringRef(e.data(), e.size()).substr(off));
+}
+
+TEST_F(WireEncoderTest, Write16) {
+  size_t off = BUFSIZE - 2;
+  WireEncoder e(0x0300u);
+  for (size_t i = 0; i < off; ++i) e.Write8(0u);  // test across Reserve()
+  e.Write16(5u);
+  e.Write16(0x10001u);  // should be truncated
+  e.Write16(0x4567u);
+  e.Write16(0u);
+  ASSERT_EQ(8u, e.size() - off);
+  ASSERT_EQ(wpi::StringRef("\x00\x05\x00\x01\x45\x67\x00\x00", 8),
+            wpi::StringRef(e.data(), e.size()).substr(off));
+}
+
+TEST_F(WireEncoderTest, Write32) {
+  size_t off = BUFSIZE - 4;
+  WireEncoder e(0x0300u);
+  for (size_t i = 0; i < off; ++i) e.Write8(0u);  // test across Reserve()
+  e.Write32(5ul);
+  e.Write32(1ul);
+  e.Write32(0xabcdul);
+  e.Write32(0x12345678ul);
+  e.Write32(0ul);
+  ASSERT_EQ(20u, e.size() - off);
+  ASSERT_EQ(wpi::StringRef("\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\xab\xcd"
+                           "\x12\x34\x56\x78\x00\x00\x00\x00",
+                           20),
+            wpi::StringRef(e.data(), e.size()).substr(off));
+}
+
+TEST_F(WireEncoderTest, WriteDouble) {
+  size_t off = BUFSIZE - 8;
+  WireEncoder e(0x0300u);
+  for (size_t i = 0; i < off; ++i) e.Write8(0u);  // test across Reserve()
+  e.WriteDouble(0.0);
+  e.WriteDouble(2.3e5);
+  e.WriteDouble(std::numeric_limits<double>::infinity());
+  e.WriteDouble(DBL_MIN);
+  e.WriteDouble(DBL_MAX);
+  ASSERT_EQ(40u, e.size() - off);
+  // golden values except min and max from
+  // http://www.binaryconvert.com/result_double.html
+  ASSERT_EQ(wpi::StringRef("\x00\x00\x00\x00\x00\x00\x00\x00"
+                           "\x41\x0c\x13\x80\x00\x00\x00\x00"
+                           "\x7f\xf0\x00\x00\x00\x00\x00\x00"
+                           "\x00\x10\x00\x00\x00\x00\x00\x00"
+                           "\x7f\xef\xff\xff\xff\xff\xff\xff",
+                           40),
+            wpi::StringRef(e.data(), e.size()).substr(off));
+}
+
+TEST_F(WireEncoderTest, WriteUleb128) {
+  size_t off = BUFSIZE - 2;
+  WireEncoder e(0x0300u);
+  for (size_t i = 0; i < off; ++i) e.Write8(0u);  // test across Reserve()
+  e.WriteUleb128(0ul);
+  e.WriteUleb128(0x7ful);
+  e.WriteUleb128(0x80ul);
+  ASSERT_EQ(4u, e.size() - off);
+  ASSERT_EQ(wpi::StringRef("\x00\x7f\x80\x01", 4),
+            wpi::StringRef(e.data(), e.size()).substr(off));
+}
+
+TEST_F(WireEncoderTest, WriteType) {
+  size_t off = BUFSIZE - 1;
+  WireEncoder e(0x0300u);
+  for (size_t i = 0; i < off; ++i) e.Write8(0u);  // test across Reserve()
+  e.WriteType(NT_BOOLEAN);
+  e.WriteType(NT_DOUBLE);
+  e.WriteType(NT_STRING);
+  e.WriteType(NT_RAW);
+  e.WriteType(NT_BOOLEAN_ARRAY);
+  e.WriteType(NT_DOUBLE_ARRAY);
+  e.WriteType(NT_STRING_ARRAY);
+  e.WriteType(NT_RPC);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(8u, e.size() - off);
+  ASSERT_EQ(wpi::StringRef("\x00\x01\x02\x03\x10\x11\x12\x20", 8),
+            wpi::StringRef(e.data(), e.size()).substr(off));
+}
+
+TEST_F(WireEncoderTest, WriteTypeError) {
+  WireEncoder e(0x0200u);
+  e.WriteType(NT_UNASSIGNED);
+  EXPECT_EQ(0u, e.size());
+  EXPECT_EQ(std::string("unrecognized type"), e.error());
+
+  e.Reset();
+  e.WriteType(NT_RAW);
+  EXPECT_EQ(0u, e.size());
+  EXPECT_EQ(std::string("raw type not supported in protocol < 3.0"), e.error());
+
+  e.Reset();
+  e.WriteType(NT_RPC);
+  EXPECT_EQ(0u, e.size());
+  EXPECT_EQ(std::string("RPC type not supported in protocol < 3.0"), e.error());
+}
+
+TEST_F(WireEncoderTest, Reset) {
+  WireEncoder e(0x0300u);
+  e.WriteType(NT_UNASSIGNED);
+  EXPECT_NE(nullptr, e.error());
+  e.Reset();
+  EXPECT_EQ(nullptr, e.error());
+
+  e.Write8(0u);
+  EXPECT_EQ(1u, e.size());
+  e.Reset();
+  EXPECT_EQ(0u, e.size());
+}
+
+TEST_F(WireEncoderTest, GetValueSize2) {
+  WireEncoder e(0x0200u);
+  EXPECT_EQ(0u, e.GetValueSize(*v_empty));  // empty
+  EXPECT_EQ(1u, e.GetValueSize(*v_boolean));
+  EXPECT_EQ(8u, e.GetValueSize(*v_double));
+  EXPECT_EQ(7u, e.GetValueSize(*v_string));
+  EXPECT_EQ(0u, e.GetValueSize(*v_raw));  // not supported
+
+  EXPECT_EQ(1u + 3u, e.GetValueSize(*v_boolean_array));
+  // truncated
+  EXPECT_EQ(1u + 255u, e.GetValueSize(*v_boolean_array_big));
+
+  EXPECT_EQ(1u + 2u * 8u, e.GetValueSize(*v_double_array));
+  // truncated
+  EXPECT_EQ(1u + 255u * 8u, e.GetValueSize(*v_double_array_big));
+
+  EXPECT_EQ(1u + 7u + 9u, e.GetValueSize(*v_string_array));
+  // truncated
+  EXPECT_EQ(1u + 255u * 3u, e.GetValueSize(*v_string_array_big));
+}
+
+TEST_F(WireEncoderTest, WriteBooleanValue2) {
+  WireEncoder e(0x0200u);
+  e.WriteValue(*v_boolean);
+  auto v_false = Value::MakeBoolean(false);
+  e.WriteValue(*v_false);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(2u, e.size());
+  ASSERT_EQ(wpi::StringRef("\x01\x00", 2), wpi::StringRef(e.data(), e.size()));
+}
+
+TEST_F(WireEncoderTest, WriteDoubleValue2) {
+  WireEncoder e(0x0200u);
+  e.WriteValue(*v_double);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(8u, e.size());
+  ASSERT_EQ(wpi::StringRef("\x3f\xf0\x00\x00\x00\x00\x00\x00", 8),
+            wpi::StringRef(e.data(), e.size()));
+}
+
+TEST_F(WireEncoderTest, WriteStringValue2) {
+  WireEncoder e(0x0200u);
+  e.WriteValue(*v_string);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(7u, e.size());
+  ASSERT_EQ(wpi::StringRef("\x00\x05hello", 7),
+            wpi::StringRef(e.data(), e.size()));
+}
+
+TEST_F(WireEncoderTest, WriteBooleanArrayValue2) {
+  WireEncoder e(0x0200u);
+  e.WriteValue(*v_boolean_array);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(1u + 3u, e.size());
+  ASSERT_EQ(wpi::StringRef("\x03\x00\x01\x00", 4),
+            wpi::StringRef(e.data(), e.size()));
+
+  // truncated
+  e.Reset();
+  e.WriteValue(*v_boolean_array_big);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(1u + 255u, e.size());
+  ASSERT_EQ(wpi::StringRef("\xff\x00", 2), wpi::StringRef(e.data(), 2));
+}
+
+TEST_F(WireEncoderTest, WriteDoubleArrayValue2) {
+  WireEncoder e(0x0200u);
+  e.WriteValue(*v_double_array);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(1u + 2u * 8u, e.size());
+  ASSERT_EQ(wpi::StringRef("\x02\x3f\xe0\x00\x00\x00\x00\x00\x00"
+                           "\x3f\xd0\x00\x00\x00\x00\x00\x00",
+                           17),
+            wpi::StringRef(e.data(), e.size()));
+
+  // truncated
+  e.Reset();
+  e.WriteValue(*v_double_array_big);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(1u + 255u * 8u, e.size());
+  ASSERT_EQ(wpi::StringRef("\xff\x00", 2), wpi::StringRef(e.data(), 2));
+}
+
+TEST_F(WireEncoderTest, WriteStringArrayValue2) {
+  WireEncoder e(0x0200u);
+  e.WriteValue(*v_string_array);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(1u + 7u + 9u, e.size());
+  ASSERT_EQ(wpi::StringRef("\x02\x00\x05hello\x00\x07goodbye", 17),
+            wpi::StringRef(e.data(), e.size()));
+
+  // truncated
+  e.Reset();
+  e.WriteValue(*v_string_array_big);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(1u + 255u * 3u, e.size());
+  ASSERT_EQ(wpi::StringRef("\xff\x00\x01", 3), wpi::StringRef(e.data(), 3));
+}
+
+TEST_F(WireEncoderTest, WriteValueError2) {
+  WireEncoder e(0x0200u);
+  e.WriteValue(*v_empty);  // empty
+  ASSERT_EQ(0u, e.size());
+  ASSERT_NE(nullptr, e.error());
+
+  e.Reset();
+  e.WriteValue(*v_raw);  // not supported
+  ASSERT_EQ(0u, e.size());
+  ASSERT_NE(nullptr, e.error());
+}
+
+TEST_F(WireEncoderTest, GetValueSize3) {
+  WireEncoder e(0x0300u);
+  EXPECT_EQ(0u, e.GetValueSize(*v_empty));  // empty
+  EXPECT_EQ(1u, e.GetValueSize(*v_boolean));
+  EXPECT_EQ(8u, e.GetValueSize(*v_double));
+  EXPECT_EQ(6u, e.GetValueSize(*v_string));
+  EXPECT_EQ(6u, e.GetValueSize(*v_raw));
+
+  EXPECT_EQ(1u + 3u, e.GetValueSize(*v_boolean_array));
+  // truncated
+  EXPECT_EQ(1u + 255u, e.GetValueSize(*v_boolean_array_big));
+
+  EXPECT_EQ(1u + 2u * 8u, e.GetValueSize(*v_double_array));
+  // truncated
+  EXPECT_EQ(1u + 255u * 8u, e.GetValueSize(*v_double_array_big));
+
+  EXPECT_EQ(1u + 6u + 8u, e.GetValueSize(*v_string_array));
+  // truncated
+  EXPECT_EQ(1u + 255u * 2u, e.GetValueSize(*v_string_array_big));
+}
+
+TEST_F(WireEncoderTest, WriteBooleanValue3) {
+  WireEncoder e(0x0300u);
+  e.WriteValue(*v_boolean);
+  auto v_false = Value::MakeBoolean(false);
+  e.WriteValue(*v_false);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(2u, e.size());
+  ASSERT_EQ(wpi::StringRef("\x01\x00", 2), wpi::StringRef(e.data(), e.size()));
+}
+
+TEST_F(WireEncoderTest, WriteDoubleValue3) {
+  WireEncoder e(0x0300u);
+  e.WriteValue(*v_double);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(8u, e.size());
+  ASSERT_EQ(wpi::StringRef("\x3f\xf0\x00\x00\x00\x00\x00\x00", 8),
+            wpi::StringRef(e.data(), e.size()));
+}
+
+TEST_F(WireEncoderTest, WriteStringValue3) {
+  WireEncoder e(0x0300u);
+  e.WriteValue(*v_string);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(6u, e.size());
+  ASSERT_EQ(wpi::StringRef("\x05hello", 6), wpi::StringRef(e.data(), e.size()));
+}
+
+TEST_F(WireEncoderTest, WriteRawValue3) {
+  WireEncoder e(0x0300u);
+  e.WriteValue(*v_raw);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(6u, e.size());
+  ASSERT_EQ(wpi::StringRef("\x05hello", 6), wpi::StringRef(e.data(), e.size()));
+}
+
+TEST_F(WireEncoderTest, WriteBooleanArrayValue3) {
+  WireEncoder e(0x0300u);
+  e.WriteValue(*v_boolean_array);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(1u + 3u, e.size());
+  ASSERT_EQ(wpi::StringRef("\x03\x00\x01\x00", 4),
+            wpi::StringRef(e.data(), e.size()));
+
+  // truncated
+  e.Reset();
+  e.WriteValue(*v_boolean_array_big);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(1u + 255u, e.size());
+  ASSERT_EQ(wpi::StringRef("\xff\x00", 2), wpi::StringRef(e.data(), 2));
+}
+
+TEST_F(WireEncoderTest, WriteDoubleArrayValue3) {
+  WireEncoder e(0x0300u);
+  e.WriteValue(*v_double_array);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(1u + 2u * 8u, e.size());
+  ASSERT_EQ(wpi::StringRef("\x02\x3f\xe0\x00\x00\x00\x00\x00\x00"
+                           "\x3f\xd0\x00\x00\x00\x00\x00\x00",
+                           17),
+            wpi::StringRef(e.data(), e.size()));
+
+  // truncated
+  e.Reset();
+  e.WriteValue(*v_double_array_big);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(1u + 255u * 8u, e.size());
+  ASSERT_EQ(wpi::StringRef("\xff\x00", 2), wpi::StringRef(e.data(), 2));
+}
+
+TEST_F(WireEncoderTest, WriteStringArrayValue3) {
+  WireEncoder e(0x0300u);
+  e.WriteValue(*v_string_array);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(1u + 6u + 8u, e.size());
+  ASSERT_EQ(wpi::StringRef("\x02\x05hello\x07goodbye", 15),
+            wpi::StringRef(e.data(), e.size()));
+
+  // truncated
+  e.Reset();
+  e.WriteValue(*v_string_array_big);
+  ASSERT_EQ(nullptr, e.error());
+  ASSERT_EQ(1u + 255u * 2u, e.size());
+  ASSERT_EQ(wpi::StringRef("\xff\x01", 2), wpi::StringRef(e.data(), 2));
+}
+
+TEST_F(WireEncoderTest, WriteValueError3) {
+  WireEncoder e(0x0300u);
+  e.WriteValue(*v_empty);  // empty
+  ASSERT_EQ(0u, e.size());
+  ASSERT_NE(nullptr, e.error());
+}
+
+TEST_F(WireEncoderTest, GetStringSize2) {
+  // 2-byte length
+  WireEncoder e(0x0200u);
+  EXPECT_EQ(7u, e.GetStringSize(s_normal));
+  EXPECT_EQ(130u, e.GetStringSize(s_long));
+  // truncated
+  EXPECT_EQ(65537u, e.GetStringSize(s_big));
+}
+
+TEST_F(WireEncoderTest, WriteString2) {
+  WireEncoder e(0x0200u);
+  e.WriteString(s_normal);
+  EXPECT_EQ(nullptr, e.error());
+  EXPECT_EQ(7u, e.size());
+  EXPECT_EQ(wpi::StringRef("\x00\x05hello", 7),
+            wpi::StringRef(e.data(), e.size()));
+
+  e.Reset();
+  e.WriteString(s_long);
+  EXPECT_EQ(nullptr, e.error());
+  ASSERT_EQ(130u, e.size());
+  EXPECT_EQ(wpi::StringRef("\x00\x80**", 4), wpi::StringRef(e.data(), 4));
+  EXPECT_EQ('*', e.data()[128]);
+  EXPECT_EQ('x', e.data()[129]);
+
+  // truncated
+  e.Reset();
+  e.WriteString(s_big);
+  EXPECT_EQ(nullptr, e.error());
+  ASSERT_EQ(65537u, e.size());
+  EXPECT_EQ(wpi::StringRef("\xff\xff**", 4), wpi::StringRef(e.data(), 4));
+  EXPECT_EQ('*', e.data()[65535]);
+  EXPECT_EQ('x', e.data()[65536]);
+}
+
+TEST_F(WireEncoderTest, GetStringSize3) {
+  // leb128-encoded length
+  WireEncoder e(0x0300u);
+  EXPECT_EQ(6u, e.GetStringSize(s_normal));
+  EXPECT_EQ(130u, e.GetStringSize(s_long));
+  EXPECT_EQ(65540u, e.GetStringSize(s_big));
+}
+
+TEST_F(WireEncoderTest, WriteString3) {
+  WireEncoder e(0x0300u);
+  e.WriteString(s_normal);
+  EXPECT_EQ(nullptr, e.error());
+  EXPECT_EQ(6u, e.size());
+  EXPECT_EQ(wpi::StringRef("\x05hello", 6), wpi::StringRef(e.data(), e.size()));
+
+  e.Reset();
+  e.WriteString(s_long);
+  EXPECT_EQ(nullptr, e.error());
+  ASSERT_EQ(130u, e.size());
+  EXPECT_EQ(wpi::StringRef("\x80\x01**", 4), wpi::StringRef(e.data(), 4));
+  EXPECT_EQ('*', e.data()[128]);
+  EXPECT_EQ('x', e.data()[129]);
+
+  // NOT truncated
+  e.Reset();
+  e.WriteString(s_big);
+  EXPECT_EQ(nullptr, e.error());
+  ASSERT_EQ(65540u, e.size());
+  EXPECT_EQ(wpi::StringRef("\x81\x80\x04*", 4), wpi::StringRef(e.data(), 4));
+  EXPECT_EQ('*', e.data()[65536]);
+  EXPECT_EQ('x', e.data()[65537]);
+  EXPECT_EQ('x', e.data()[65538]);
+  EXPECT_EQ('x', e.data()[65539]);
+}
+
+}  // namespace nt
diff --git a/ntcore/src/test/native/cpp/main.cpp b/ntcore/src/test/native/cpp/main.cpp
new file mode 100644
index 0000000..d0b0e3c
--- /dev/null
+++ b/ntcore/src/test/native/cpp/main.cpp
@@ -0,0 +1,23 @@
+/*----------------------------------------------------------------------------*/
+/* Copyright (c) 2015-2018 FIRST. All Rights Reserved.                        */
+/* Open Source Software - may be modified and shared by FRC teams. The code   */
+/* must be accompanied by the FIRST BSD license file in the root directory of */
+/* the project.                                                               */
+/*----------------------------------------------------------------------------*/
+
+#include <climits>
+
+#include "gmock/gmock.h"
+#include "ntcore.h"
+
+int main(int argc, char** argv) {
+  nt::AddLogger(nt::GetDefaultInstance(),
+                [](const nt::LogMessage& msg) {
+                  std::fputs(msg.message.c_str(), stderr);
+                  std::fputc('\n', stderr);
+                },
+                0, UINT_MAX);
+  ::testing::InitGoogleMock(&argc, argv);
+  int ret = RUN_ALL_TESTS();
+  return ret;
+}