Overview
Typeshare is a tool for generating type definitions in other languages based on type definitions in Rust. Specifically, Typeshare is useful for FFI where types are passed as fully serialized blobs, and then decoded on the other side.
Typeshare was originally developed as an internal tool at 1Password, but has been released as an open-source crate that you can use and contribute to.
This guide describes how to install and use Typeshare effectively.
Installation
The easiest way to install the Typeshare CLI is with cargo
. Just run the following command:
cargo install typeshare-cli
Once you have the CLI installed, you then need to annotate the rust types that you want to generate FFI definitions for. In order to be able to use the #[typeshare]
annotation, you will need to add typeshare
as a dependency to your project's Cargo.toml
.
# Cargo.toml
[dependencies]
typeshare = "1.0.0" # Use whichever version is the most recent
Usage
This section will cover how to use Typeshare to generate type definitions for your project and how to interact with them across FFI.
Typeshare provides a CLI tool that will perform the necessary generation for any types you annotate in your Rust code.
To generate ffi definitions for a specific target language, run the typeshare
command and specify the directory containing your rust code, the language you would like to generate for, and the file to which your generated definitions will be written:
typeshare ./my_rust_project --lang=kotlin --output-file=my_kotlin_definitions.kt
typeshare ./my_rust_project --lang=swift --output-file=my_swift_definitions.swift
typeshare ./my_rust_project --lang=typescript --output-file=my_typescript_definitions.ts
typeshare ./my_rust_project --lang=scala --output-file=my_scala_definitions.scala
The first command-line argument is the name of the directory to search for Rust type definitions. The CLI will search all files in the specified directory tree for annotated Rust types. In addition to the input directory, you will also need to specify your desired target language and the output file to which the generated types will be written. This is done with the --lang
and --output-file
options respectively.
The currently supported output languages are:
- Kotlin
- Typescript
- Swift
- Scala
- Go
If your favourite language is not in this list, consider opening an issue to request it or try implementing it yourself! See our contribution guidelines for more details.
In the following sections, we will learn how to customize the behaviour of Typeshare using the provided #[typeshare]
attribute and configuration options.
Annotations
Annotating Types
Add the #[typeshare]
attribute to any struct or enum you define to generate definitions for that type in the selected output language.
#![allow(unused)] fn main() { #[typeshare] struct MyStruct { my_name: String, my_age: u32, } #[typeshare] enum MyEnum { MyVariant, MyOtherVariant, MyNumber(u32), } }
Annotation arguments
We can add arguments to the #[typeshare]
annotation to modify the generated definitions.
Decorators
It can be used to add decorators like Swift protocols or Kotlin interfaces to the generated output types. For example, we can do
#![allow(unused)] fn main() { #[typeshare(swift = "Equatable, Codable, Comparable, Hashable")] #[serde(tag = "type", content = "content")] pub enum BestHockeyTeams { MontrealCanadiens, Lies(String), } }
and this will produce the following Swift definition:
public enum OPBestHockeyTeams2: Codable, Comparable, Equatable, Hashable {
case montrealCanadiens
case lies(String)
enum CodingKeys: String, CodingKey, Codable {
case montrealCanadiens = "MontrealCanadiens",
lies = "Lies"
}
private enum ContainerCodingKeys: String, CodingKey {
case type, content
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ContainerCodingKeys.self)
if let type = try? container.decode(CodingKeys.self, forKey: .type) {
switch type {
case .montrealCanadiens:
self = .montrealCanadiens
return
case .lies:
if let content = try? container.decode(String.self, forKey: .content) {
self = .lies(content)
return
}
}
}
throw DecodingError.typeMismatch(OPBestHockeyTeams2.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for OPBestHockeyTeams"))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: ContainerCodingKeys.self)
switch self {
case .montrealCanadiens:
try container.encode(CodingKeys.montrealCanadiens, forKey: .type)
case .lies(let content):
try container.encode(CodingKeys.lies, forKey: .type)
try container.encode(content, forKey: .content)
}
}
}
Serialize as Another Type
You can also use the serialized_as
argument to tell Typeshare to treat
the serialized type as another Rust type. This is usually combined with
custom serde attributes.
#![allow(unused)] fn main() { /// Options that you could pick #[typeshare(serialized_as = "String")] #[serde(try_from = "String", into = "String")] pub enum Options { /// Affirmative Response Yes, No, Maybe, /// Sends a string along Cool(String), } }
This would generate the following Kotlin code:
/// Options that you could pick
typealias Options = String
The #[serde]
Attribute
Since Typeshare relies on the serde
crate for handling serialization and deserialization between Rust types and the generated foreign type definitions, we can use the annotations provided by serde
on our Typeshare types. For example, the following Rust definition
#![allow(unused)] fn main() { /// This is a comment. /// Continued lovingly here #[typeshare] #[serde(rename_all = "camelCase")] pub enum Colors { Red = 0, Blue = 1, /// Green is a cool color #[serde(rename = "green-like")] Green = 2, } }
will become the following Typescript definition.
/**
* This is a comment.
* Continued lovingly here
*/
export const enum Colors {
Red = "red",
Blue = "blue",
/** Green is a cool color */
Green = "green-like",
}
Skipping Fields
Within a Rust type, there may be fields or variants that you want Typeshare to ignore. These can be skipped using either the #[serde(skip)]
annotation or the #[typeshare(skip)]
annotation. For example, this Rust type
#![allow(unused)] fn main() { #[typeshare] pub struct MyStruct { a: i32, #[serde(skip)] b: i32, c: i32, #[typeshare(skip)] d: i32, } }
becomes the following Typescript definition.
export interface MyStruct {
a: number;
c: number;
}
Configuration
The behaviour of Typeshare can be customized by either passing options on the command line or in a configuration file. For any command line option that corresponds to a value in the configuration file, specifying the option on the command line will override the value in the configuration file.
Command Line Options
-
-l
,--lang
(Required) The language you want your definitions to be generated in. Currently, this option can be set to eitherkotlin
,swift
,go
, ortypescript
. -
-o
,--output-file
(Required or -d) The file path to which the generated definitions will be written. -
-d
,--directory
(Required or -o) The folder path to write the multiple module files to. -
-s
,--swift-prefix
Specify a prefix that will be prepended to type names when generating types in Swift. -
-M
,--module-name
Specify the name of the Kotlin module for generated Kotlin source code. -
-t
,--target-os
Optional comma separated list of target os targets. Types that are restricted via#[cfg(target_os = <target>]
that do not match the argument list will be filtered out. -
-j
,--java-package
Specify the name of the Java package for generated Kotlin types. -
-c
,--config-file
Instead of searching for atypeshare.toml
file, this option can be set to specify the path to the configuration file that Typeshare will use. -
-g
,--generate-config-file
Instead of running Typeshare with the provided options, generate a configuration file calledtypeshare.toml
containing the options currently specified as well as default configuration parameters. -
--directories
A list argument that you can pass any number of glob patterns to. All folders and files given will be searched recursively, and all Rust sources found will be used to create a singular language source file. -
--go-package
The name of the Go package for use with building for Go. This will be included in the header of the output file. This option will only be available iftypeshare-cli
was built with thego
feature.
Configuration File
By default, Typeshare will look for a file called typeshare.toml
in your current directory or any of its parent directories. Typeshare configuration files will look like this:
[swift]
prefix = 'MyPrefix'
[kotlin]
module_name = 'myModule'
package = 'com.example.package'
[swift.type_mappings]
"DateTime" = "Date"
[typescript.type_mappings]
"DateTime" = "string"
[kotlin.type_mappings]
"DateTime" = "String"
In the configuration file, you can specify the options you want to set so that they do not need to be specified when running Typeshare from the command line. You can also define custom type mappings to specify the foreign type that a given Rust type will correspond to.
In order to create a config file you can run the following command to generate one in your current directory.
typeshare -g
Target OS
The --target-os
argument is an optional command line argument that allows you to specify a list of comma separated target_os
values. In your
Rust source code you can use the target_os
attribute
to restrict a type, variant or fields.
If you do not use --target-os
then typeshare will generate all types, variants and fields that are typeshared.
Example
./typeshare ./my_rust_project \
--lang=typescript \
--output-file=my_typescript_definitions.ts \
--target-os=linux,macos
The example above is stating any types, variants, fields that are typeshared and are not applicable for linux
or macos
will be omitted from
typeshare type generation.
Supported target_os
definitions.
Simple standalone.
#![allow(unused)] fn main() { #[cfg(target_os = "android")] pub struct MyType; }
This type will only be generated if --target-os
has android
in the list of target_os values.
Simple not rule
#![allow(unused)] fn main() { #[cfg(not(target_os = "android"))] pub struct MyType; }
This type will only be generated if --target-os
does not include android
in the list of target_os values.
Multiple not any rule
#![allow(unused)] fn main() { #[cfg(not(any(target_os = "android", target_os = "ios")))] pub struct MyType; }
This type will only be generated if --target-os
does not include android
or ios
in the list of target_os values.
The following example will allow MyType
to be typeshared.
./typeshare ./my_rust_project \
--lang=typescript \
--output-file=my_typescript_definitions.ts \
--target-os=linux,macos
The following example will not allow MyType
to be typeshared.
./typeshare ./my_rust_project \
--lang=typescript \
--output-file=my_typescript_definitions.ts \
--target-os=android,macos
Combined with features or other cfg attributes
Typehsare will not take into consideration any other cfg
attributes other than target_os
when generating types.
For example:
#![allow(unused)] fn main() { #[cfg(any(target_os = "android", feature = "android-test")] pub struct MyType; }
#![allow(unused)] fn main() { #[cfg(all(target_os = "android", feature = "android-test")] pub struct MyType; }
./typeshare ./my_rust_project \
--lang=typescript \
--output-file=my_typescript_definitions.ts \
--target-os=android
In both examples above, MyType
will be typeshared.
Contributing to Typeshare
Thank you! If you would like to contribute to the project, go to the GitHub repository. There, you can open issues to report bugs and request features.
If you want to contribute code, please send a GitHub Pull Request to the repository with a clear list of your changes and the reasons for the changes. Before contributing, you should probably familiarize yourself with the internal documentation, if you haven't already. Make sure to look through the READMEs for each part of Typeshare you plan to contribute to, and double-check the Rust documentation on docs.rs
if you want to know more about the API. And feel free to open an issue if you want to ask the 1Password team directly, or if the documentation is unclear.
For larger changes, please open an RFC issue first in order to discuss the broader impacts of the proposed change with the other maintainers.