# objc2: Objective-C and Apple framework bindings rebuilt as Rust crates

> objc2 is a Rust binding layer for Objective-C and the Apple frameworks, assembled by merging six earlier crates into one workspace with generated framework crates for AppKit, UIKit, Foundation and the rest. It models Objective-C protocols as Rust traits, denies unsafe operations inside unsafe functions, offers a choice of three permissive licences, and has no tagged releases at all.

**madsmtm/objc2** — Bindings to Apple frameworks in Rust

- Repository: https://github.com/madsmtm/objc2
- Website: https://docs.rs/objc2
- Stars: 1,077 · Forks: 86
- Language: Rust
- License: NOASSERTION
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/madsmtm-objc2

## Five crates merged into one workspace

objc2 provides bindings to Apple frameworks in Rust, and the project description in the README is Objective-C in Rust.

The history explains the current shape. This repository is a merge of several earlier projects, with the reasoning for the fork recorded in a linked issue. The `objc` crate was renamed to `objc2`. The `objc-encode` crate became `objc2-encode`. `objc_exception` moved into `objc2::exception`, `objc_id` into `objc2::rc`, and `objc-foundation` was renamed to `objc2-foundation`. The `block` crate became `block2`, and the `dispatch2` crate originally lived in a separate repository.

The README credits the original author of those crates and the companion crates `block2` and `dispatch2`, which are the pieces you need for Objective-C blocks and for the Grand Central Dispatch queues respectively. A Rust program calling into an Apple framework needs all three, and the top-level documentation on docs.rs covers the framework crates as well.

The layout shows the scale of the undertaking. The workspace members are `crates/*`, `crates/test-assembly/crates/*`, `examples/*` and `framework-crates/*`, with a `generated` directory and a `framework-crates/` directory that holds the per-framework crates. The `docs/` directory holds the top-level documentation, and the `examples/` directory contains working programs rather than toy snippets, with directories for app, audio, corefoundation, doc-viewer, fskit, metal, pasteboard, security, testing-helper, testing and webkit.

The topic list gives the platform reach: appkit, cocoa, foundation, ios, macos, objc, tvos, uikit, visionos and watchos. So the target is Apple platforms across the range, and the intended user is a Rust developer who needs to reach Cocoa or an Apple framework, for a native macOS or iOS application, or a library that has to interoperate with existing Objective-C code.

## Protocols as traits, and the semver problem that creates

The most interesting design decision in the project is visible in the workspace lint configuration, under the cargo-semver-checks section, and it is worth reading closely because it explains a structural limitation.

The project models protocols as traits. The first lint records that users cannot implement them manually outside of `define_class!`, and the second is about splitting protocols into smaller protocols, or adding non-required methods, which the comment says is often done by Apple. It is not yet clear how this would be handled if the user has implemented the protocol themselves, and a linked issue number is given.

That is a genuine tension, and stating it in the manifest is the right thing to do. A Rust trait is a contract, and a contract that Apple can extend at any time is a contract that can break your build. A protocol that gains an optional method, or is split into two, changes the trait's shape, and any code that implemented it by hand has to change with it. The `define_class!` macro constrains implementations to a form the project controls, which keeps the surface predictable, at the cost of taking away the ability to implement a protocol yourself in the general case.

The same section of the manifest shows that the project treats semver compatibility as something to be checked mechanically rather than by eye. There are lint entries at warn level for the two cases above, and a `profile.assembly-tests` profile that inherits release settings and enables LTO so that the assembly level tests can exercise an unstable static selector inlining feature.

The practical reading for a user is that objc2's protocol model is stricter than Apple's, and that the project is candid about the places where that strictness will cost you. That candour is a good sign for a binding layer, where the alternative is discovering the problem yourself in a build error.

## The lint policy and the toolchain floor

The workspace lint block is short and it sets the house style for the whole tree:

```toml
[workspace.lints.rust]
elided_lifetimes_in_paths = "warn"
non_ascii_idents = "deny"
unreachable_pub = "warn"
unsafe_op_in_unsafe_fn = "deny"
```

The line that matters most is `unsafe_op_in_unsafe_fn = "deny"`. It requires every unsafe operation inside an `unsafe fn` to be wrapped in an explicit `unsafe` block, which means the compiler will not let an unsafe call slip through unremarked inside a function that is already unsafe. For a binding layer, which is unsafe code by nature and touches Objective-C's reference counting and dispatch rules, that policy is a reasonable way to keep the unsafe surface auditable.

`non_ascii_idents = "deny"` is a smaller decision with a clear rationale. Objective-C selectors and class names can contain non-ASCII characters, and Rust identifiers can too, so a project that binds them needs a deliberate policy about what to do with those names, and denying them outright forces the encoding into data rather than identifiers.

The clippy block follows the same pattern, warning on `ptr_as_ptr` and allowing `redundant_feature_names` for one specific crate, `objc2-ad-support`, where the name cannot be changed.

The toolchain requirements are stated in the same file. The workspace sets `rust-version = "1.71"`, and the comment above it explains the exception: 1.91 is required on Simulator and Mac Catalyst targets, which are described as slightly broken on earlier Rust versions. That is the kind of detail that costs an afternoon if you find it the hard way, and it is right there in the manifest.

The edition is 2021, the workspace uses resolver 2, and there is a `rust-toolchain.toml` at the root pinning the toolchain for contributors, plus a `rustfmt.toml` for formatting.

## Generated framework crates and the example programs

The `framework-crates/` directory is where the per-framework bindings live, and the documentation for them is published on docs.rs under the about_generated topic. The version for those crates is set in the workspace package section at 0.3.2, with a comment reminding the maintainers to update it in the header translator and in the manually authored `lib.rs`.

That comment is a small window into how the project works. There is a generator, there is a header translator that emits code from Apple framework headers, and there is hand written code alongside it. The generated crates and the hand written ones coexist, and the version has to be kept in step across three places.

The `examples/` directory is the more useful part for a newcomer, and the list is revealing about what this project is used for. The `app` example is an application. The `audio` example deals with audio. `corefoundation` shows the Core Foundation layer, `metal` the Metal graphics API, `pasteboard` the clipboard, `security` the security framework, `webkit` the web view, `fskit` the file system, and `doc-viewer` the document rendering. There are two testing directories, `testing` and `testing-helper`, which suggests that testing Objective-C code from Rust is a first class use case rather than an afterthought.

The `crates/test-assembly/` path inside the workspace members is the assembly level test suite, and the release profile for it exists to run those tests with link time optimisation so that the low level code paths are actually exercised.

For a user, the practical order of work is to read the top-level documentation on docs.rs, find the framework crate for the API you need, and then look at the example that does something similar to what you want. The examples are complete programs, so they show the calling convention rather than only the type signatures.

## Three licences, with per-crate exceptions

The licensing is the one place where the README is deliberately vague, and the vagueness is a warning rather than an oversight. The licensing of the project is a bit complicated, the README says, and points to `LICENSE.md` for details. The badge in the header names the three options, Zlib OR Apache-2.0 OR MIT, and the manifest repeats that as the workspace package licence.

The repository carries `LICENSE-APACHE.txt`, `LICENSE-MIT.txt` and `LICENSE-ZLIB.txt` alongside `LICENSE.md`, and the manifest adds a warning: some crates in this repo have a different license, see LICENSE.md.

So the picture is a permissive default with exceptions. All three of the named licences are permissive, with MIT the most permissive and Zlib the closest to a BSD style licence, and Apache-2.0 the one that carries an explicit patent grant. The exceptions are the part to check, because a dependency whose licence differs from the workspace default can change what you owe when you redistribute a binary that links it.

The project metadata lists the licence as other, a custom licence that GitHub cannot classify, which is consistent with a multi-licence file rather than a single standard one. This is a description of the files rather than legal advice, and the practical step is to read `LICENSE.md` before you vendor anything.

The same goes for the prior art. The acknowledgements section credits a long list of projects that inspired the design, including `apple-sys`, `cacao`, `core-foundation-rs`, `fruity`, `metal-rs`, `objrs`, `objr`, `rust-macios`, `uikit-sys`, `cidre` and `apple-media-rs`, and then lists other languages that have attempted Objective-C interop, from Swift and C# through Python, Ruby, Dart, Kotlin, Nim, D, Java, Node.js, Zig and Go. Swift is singled out as the language objc2 aspires to have feature parity with, with the note that it will probably never reach it. That is a useful calibration for anyone deciding what objc2 can do today.

## Where objc2 is the wrong tool

There are three cases where this project is the wrong answer, and the first is the simplest.

If you are not targeting an Apple platform, it does not apply. The bindings exist for Objective-C and the Apple frameworks, and there is no portability story for a Linux server or an Android application beyond the Objective-C language itself.

The second is when you need the full Objective-C language surface, or the parts objc2 deliberately does not model. The protocol-as-trait model means you cannot implement a protocol by hand outside `define_class!`, and the manifest itself flags the unresolved question of what happens when Apple splits a protocol or adds an optional method to one you have implemented. Any project that leans on protocol conformance in unusual ways will hit that wall.

The third is version stability. There are no GitHub releases for this repository, and the framework crates sit at 0.3.2, a pre-1.0 version in which the API is allowed to move between minor releases. The last push to `main` was on 2026-10-14, so the code is moving, and for a binding layer that tracks a fast moving platform SDK, that is a reasonable rate of change and a reason to pin a specific version in your `Cargo.lock` rather than track the branch.

The alternative approach, and the one to weigh against, is to use bindgen directly on the framework headers, or to hand write the `extern` declarations you need. bindgen gives you a mechanically generated layer with no opinions, at the cost of giving up the typed message sending, the ARC-like reference counting helpers and the block and dispatch support that objc2 provides. The difference in approach is between a set of raw declarations and a runtime support layer, and the second one is what makes calling into Objective-C from Rust pleasant rather than merely possible.

There is a third option for a subset of cases, and it is the one the project itself points at for a specific problem: Swift is built for Objective-C interop from the beginning, and a small Swift shim compiled to a static library can be a simpler bridge than reimplementing the runtime semantics in Rust.

## Conclusion

Adopt objc2 if you are writing Rust that has to call into Objective-C or an Apple framework, and you want typed bindings rather than hand written `extern` declarations, since the framework crates are generated and the protocol model is checked by semver tooling. Do not adopt it if you are targeting platforms other than Apple, if you need the full Objective-C language surface including things the project states it will not support, or if you need a stable published version to depend on. Verify three things first: that your Rust toolchain meets the version requirement, since the workspace declares 1.71 as the minimum but notes that 1.91 is required for Simulator and Mac Catalyst targets, that the specific framework you need has a generated crate in `framework-crates/`, and that the licence combination you pick applies to every crate you pull in, because the README notes some crates in the repository carry a different licence.

## FAQ

### What is the objc2 crate?

It provides bindings to Apple frameworks in Rust, Objective-C in Rust, and it also ships the companion `block2` and `dispatch2` crates for Objective-C blocks and dispatch queues. The repository was formed by merging the earlier objc, objc-encode, objc_exception, objc_id and objc-foundation crates into one Cargo workspace.

### How does objc2 model Objective-C protocols?

As Rust traits. The workspace manifest records that users cannot implement them manually outside of `define_class!`, and it flags the open question of how Apple splitting a protocol or adding non-required methods would be handled, with a linked issue for the details.

### Which Rust version does objc2 require?

The workspace sets rust-version 1.71, and the comment above it notes that 1.91 is required on Simulator and Mac Catalyst targets because those are slightly broken on earlier Rust versions. The edition is 2021 and the workspace uses resolver 2.

### What licence does objc2 use?

The workspace package licence is Zlib OR Apache-2.0 OR MIT, and the repository ships LICENSE-APACHE.txt, LICENSE-MIT.txt and LICENSE-ZLIB.txt with a LICENSE.md explaining the details. The manifest notes that some crates in the repository carry a different licence.

### Where are the Apple framework bindings in objc2?

In the framework-crates directory, with the documentation published on docs.rs under the about_generated topic. The examples directory shows them in use with programs for app, audio, corefoundation, doc-viewer, fskit, metal, pasteboard, security, testing and webkit.

## Sources

- [Issues](https://github.com/madsmtm/objc2/issues)
- [madsmtm/objc2 on GitHub](https://github.com/madsmtm/objc2)
- [Project website](https://docs.rs/objc2)
- [README](https://github.com/madsmtm/objc2/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/madsmtm-objc2
