惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hugging Face - Blog
Hugging Face - Blog
B
Blog
博客园_首页
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
G
GRAHAM CLULEY
Microsoft Azure Blog
Microsoft Azure Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
The GitHub Blog
The GitHub Blog
Security Latest
Security Latest
F
Full Disclosure
云风的 BLOG
云风的 BLOG
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
C
Cyber Attacks, Cyber Crime and Cyber Security
L
LINUX DO - 热门话题
V
Visual Studio Blog
有赞技术团队
有赞技术团队
腾讯CDC
V
V2EX
Vercel News
Vercel News
C
Cisco Blogs
V2EX - 技术
V2EX - 技术
Scott Helme
Scott Helme
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
AWS News Blog
AWS News Blog
S
Schneier on Security
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
小众软件
小众软件
G
Google Developers Blog
C
Check Point Blog
C
CERT Recently Published Vulnerability Notes
博客园 - 叶小钗
S
SegmentFault 最新的问题
T
Tor Project blog
J
Java Code Geeks
L
Lohrmann on Cybersecurity
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
The Exploit Database - CXSecurity.com
Apple Machine Learning Research
Apple Machine Learning Research
T
Tailwind CSS Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 司徒正美
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
Secure Thoughts
量子位
N
News and Events Feed by Topic
MyScale Blog
MyScale Blog
TaoSecurity Blog
TaoSecurity Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Engineering at Meta
Engineering at Meta

Peter Steinberger

OpenClaw, OpenAI and the future | Peter Steinberger Shipping at Inference-Speed | Peter Steinberger The Signature Flicker | Peter Steinberger Just Talk To It - the no-bs Way of Agentic Engineering | Peter Steinberger Claude Code Anonymous | Peter Steinberger Live Coding Session: Building Arena | Peter Steinberger My Current AI Dev Workflow | Peter Steinberger Essential Reading for Agentic Engineers - August 2025 | Peter Steinberger Just One More Prompt | Peter Steinberger Poltergeist: The Ghost That Keeps Your Builds Fresh | Peter Steinberger Don't read this Startup Slop | Peter Steinberger Essential Reading for Agentic Engineers - July 2025 | Peter Steinberger Self-Hosting AI Models After Claude's Usage Limits | Peter Steinberger Logging Privacy Shenanigans | Peter Steinberger VibeTunnel's first AI-anniversary | Peter Steinberger Making AppleScript Work in macOS CLI Tools: The Undocumented Parts | Peter Steinberger Peekaboo 2.0 – Free the CLI from its MCP shackles | Peter Steinberger Command your Claude Code Army, Reloaded | Peter Steinberger Essential Reading for Agentic Engineers | Peter Steinberger Slot Machines for Programmers: How Peter Builds Apps 20x Faster with AI | Peter Steinberger My AI Workflow for Understanding Any Codebase | Peter Steinberger stats.store: Privacy-First Sparkle Analytics | Peter Steinberger Showing Settings from macOS Menu Bar Items: A 5-Hour Journey | Peter Steinberger VibeTunnel: Turn Any Browser into Your Mac's Terminal | Peter Steinberger Vibe Meter 2.0: Calculating Claude Code Usage with Token Counting | Peter Steinberger llm.codes: Make Apple Docs AI-Readable | Peter Steinberger Automatic Observation Tracking in UIKit and AppKit: The Feature Apple Forgot to Mention | Peter Steinberger Peekaboo MCP – lightning-fast macOS screenshots for AI agents | Peter Steinberger Migrating 700+ Tests to Swift Testing: A Real-World Experience | Peter Steinberger Commanding Your Claude Code Army | Peter Steinberger Code Signing and Notarization: Sparkle and Tears | Peter Steinberger Vibe Meter: Monitor Your AI Costs | Peter Steinberger Claude Code is My Computer | Peter Steinberger Stop Over-thinking AI Subscriptions | Peter Steinberger Introducing Demark: HTML in. MD out. Blink-fast. | Peter Steinberger The Future of Vibe Coding: Building with AI, Live and Unfiltered | Peter Steinberger MCP Best Practices | Peter Steinberger Finding My Spark Again | Peter Steinberger Top-Level Menu Visibility in SwiftUI for macOS | Peter Steinberger Fixing keyboardShortcut in SwiftUI | Peter Steinberger Supporting Both Tap and Long Press on a Button in SwiftUI | Peter Steinberger On Using Apple Silicon Mac Mini for Continuous Integration | Peter Steinberger Apple Silicon M1: A Developer's Perspective | Peter Steinberger Gardening Your Twitter: Curating Your Timeline | Peter Steinberger Gardening Your Twitter: Growing Your Followers | Peter Steinberger Forbidden Controls in Catalyst: Optimize Interface for Mac | Peter Steinberger Disabling Keyboard Avoidance in SwiftUI's UIHostingController | Peter Steinberger The State of SwiftUI | Peter Steinberger Logging in Swift | Peter Steinberger Building with Swift Trunk Development Snapshots | Peter Steinberger Calling Super at Runtime in Swift | Peter Steinberger zld — A Faster Version of Apple's Linker | Peter Steinberger How to Fix LLDB: Couldn't IRGen Expression | Peter Steinberger Updating macOS on a Hackintosh | Peter Steinberger InterposeKit — Elegant Swizzling in Swift | Peter Steinberger The Great Mac Catalyst Text Input Crash Hunt | Peter Steinberger Jailbreaking for iOS Developers | Peter Steinberger Network Kernel Core Dump | Peter Steinberger How to macOS Core Dump | Peter Steinberger Kernel Panics and Surprise boot-args | Peter Steinberger The LG UltraFine 5K, kernel_task, and Me | Peter Steinberger Let's Try This Again | Peter Steinberger How We Work at PSPDFKit | Peter Steinberger Swizzling in Swift | Peter Steinberger WWDC for First-Timers, 2019 Edition | Peter Steinberger Challenges of Adopting Drag and Drop | Peter Steinberger Marzipan: Porting iOS Apps to the Mac | Peter Steinberger How to Use Slack and Not Go Crazy | Peter Steinberger Hardcore Debugging - Heavy Weapons for Hard Bugs | Peter Steinberger Binary Frameworks in Swift | Peter Steinberger Even Swiftier Objective-C | Peter Steinberger The Case for Deprecating UITableView | Peter Steinberger Running tests with Clang Address Sanitizer | Peter Steinberger UI testing on iOS, without busy waiting | Peter Steinberger Hiring a distributed team | Peter Steinberger Writing Good Bug Reports | Peter Steinberger Real-time collaboration, Apple, and you | Peter Steinberger Converting Xcode Test Runs to JUnit, the Fast Way | Peter Steinberger Efficient iOS Version Checking | Peter Steinberger Investigating Thread Safety of UIImage | Peter Steinberger Swifty Objective-C | Peter Steinberger Running UI Tests on iOS With Ludicrous Speed | Peter Steinberger Surprises with Swift Extensions | Peter Steinberger Using ccache for Fun and Profit | Peter Steinberger UITableViewController designated initializer woes | Peter Steinberger Researching ResearchKit | Peter Steinberger The curious case of rotation with multiple windows on iOS 8 | Peter Steinberger UIKit Debug Mode | Peter Steinberger Retrofitting containsString: on iOS 7 | Peter Steinberger A Story About Swizzling "the Right Way™" and Touch Forwarding | Peter Steinberger Hacking with Aspects | Peter Steinberger Fixing UITextView On iOS 7 | Peter Steinberger Fixing What Apple Doesn't | Peter Steinberger How To Inspect The View Hierarchy Of Third-Party Apps | Peter Steinberger Fixing UISearchDisplayController On iOS 7 | Peter Steinberger Smart Proxy Delegation | Peter Steinberger Adding Keyboard Shortcuts To UIAlertView | Peter Steinberger How To Center Content Within UIScrollView | Peter Steinberger UIAppearance for Custom Views | Peter Steinberger Hacking Block Support Into UIMenuItem | Peter Steinberger
A Pragmatic Approach to Cross-Platform | Peter Steinberger
Peter Steinberger · 2016-04-13 · via Peter Steinberger

At PSPDFKit we offer native SDKs for iOS, Android and soon also for the browser. Yet, invisible to the user, all share a large part of the codebase. This wasn’t always the case. Here’s what we learned over the last few years.

Humble Beginning

PSPDFKit started in 2010 as an iOS-only project. Back then there was just one person working on it. 6 years later, we’re 28 people, have frameworks for iOS, Android and the Web, and our core framework even runs on OS X and Windows.

In 2014 when we started working on our Android SDK, the existing codebase was a mix of Objective-C and C (for the performance critical parts), and it was highly dependent on Apple’s Core Graphics PDF renderer. We had to start fresh for Android — yet it was clear that we couldn’t afford to maintain two completely separate codebases: this would simply not scale. As a small, bootstrapped company we had to find an approach where we can re-use most of our codebase.

Choosing The Right Strategy

Everyone knows that cross-platform is hard — furthermore many applications that are cross-platform ultimately deliver a sub-par experience on all platforms. One of the main selling points of our SDK is that it’s really native and feels at home on the platform, like something Apple or Google would put on there. Compromising on this experience was not an option.

There are various projects that try to abstract the user interface across multiple platforms for apps, such as Appcelerator Titanium, Xamarin.Forms, Apache Cordova or — very promising — Facebook’s React Native. We have some experience with all of them, as we offer wrappers that make it simpler to use and configure PSPDFKit within these products. Yet for an SDK, none of these solutions work. They are all more or less compromises and also incur overhead and additional complexity. At least the UI-part had to be native to the platform.

A perfect candidate for a shared module is the PDF renderer. We actually wanted to go further and share licensing, parsing, networking and most of our business logic. Initially, we flirted with the idea of using Apportable to cross-compile existing code to Android, however after an initial chat it was clear that their service is more tailored to games, and many of the frameworks we use were missing from Android or incomplete. There were also obvious licensing questions and worries about betting so heavily on a company that is VC-backed. In the end they didn’t even send us their SDK to evaluate, so we quickly moved away from that idea. In retrospect, this was great, since while Apportable would have helped us to get to Android faster, we would be stuck again when porting code to Linux for PSPDFKit for the Web.

Choosing The Right Language

With Objective-C out of the question, C or C++ where pretty much the only languages that were worth considering that could be compiled to any platform. Lua didn’t feel powerful enough for a project of our size, and Swift isn’t mature enough yet, even though work is underway to bring it to Android and make it easier to use as a server language. There are projects such as RoboVM that run Java on iOS or even scarier solutions such as j2objc that cross-compile Java source code to Objective-C sources, however this adds a lot of complexity and again would be a vendor lock-in. JavaScript is something to consider as it has a powerful ecosystem and there are many interpreters available that make it almost as flexible as C++. However, parsing and rendering performance is a major factor for us and it’s still significantly slower than C++, plus one has to pay a startup tax while it’s being parsed.

C++ is a pragmatic solution and a first-class citizen on almost every platform. Furthermore, we were intrigued by the advances of C++ in form of C++ 11, 14 and 17. Many major projects that we use every day are built in C++: WebKit, Clang — even Swift itself. There’s great support for it on iOS: Objective-C and C++ can be freely mixed, and Google offers an NDK that makes it relatively simply to build native libraries for Android.

Especially for Android, there are also quite some downsides compared to pure Java or even JavaScript. Adding a native library greatly increases complexity and compile times, and also makes you think about which platforms should be supported. There’s also the problem of debugging: while Android Studio has made some steps in that direction, there’s still no way to see a stack trace from Java down to your C++ code. You’re basically left with old-school printf() debugging.

Android Inception

As a PDF library, our requirement in the beginning were mostly to parse data fast, and then bring them from C++ to Java. Think about page links, outlines, page labels, page sizes, images and so on. Java uses a bridge called Java Native Interface (JNI) to bring managed code to native code and vice versa, and everyone who wrote such code before knows that it’s painful. We started using Google’s FlatBuffers to automatically marshal data structures in C++, pack them up, send them as one junk to Java and then unpack them there to limit our JNI surface area. This turned out to work great and is really fast. Flatbuffers includes a code generator that generates all the marshaling code from a schema. It’s also a mature and widely used project, so it was quite easy to integrate.

Moving The iOS SDK To C++

After we’ve shipped Android 1.0, we started thinking about how we can move our iOS SDK over to use the new ‘PSPDFCore’, as it is called internally. The goal was to change as little public API as possible. However we didn’t want to expose any C++ to our customers. One of our goals is to deliver a product with an API that feels just like the system APIs. Using C++ would require knowledge that not every iOS developer has. It would also create an API that you cannot use from Swift, so that’s out of the question.

Luckily, around that time Dropbox announced Djinni. Djinni is an IDL that creates boilerplate code for you. You define your model and interface in this little language, and the djinni parser builds C++, Java and Objective-C++ boilerplate that translates between the languages.

Here are some examples:

OutlineElement = interface +c {
    const get_level(): i64;
    const get_action(): optional<Action>;
    const get_title(): string;
    const get_children(): list<OutlineElement>;
    const get_color(): optional<Color>;
    const is_bold_style(): bool;
    const is_italic_style(): bool;
}

OutlineParser = interface +c {
    const get_outline(): optional<OutlineElement>;
}

The get_color() call returns a Color object. In C++ we represent Color as a class. In Java we just want to use int since that’s a common pattern on Android and reduces the workload on the garbage collector. And for Objective-C/Swift on iOS, we’d want to use UIColor. By default, Djinni would create a PDFCColor class however, and a Color class on Java. But we can go one step further and create custom bridged data types as well:

---
name: Color
typedef: 'record'
params: []
prefix: ''
cpp:
  typename: 'PDFC::Color'
  header: '"Basic/Color.hpp"'
  byValue: false
objc:
  typename: 'UIColor'
  header: '"PSPDFEnvironment.h"'
  boxed: 'UIColor'
  pointer: true
  hash: '(NSUInteger)%s'
  printDescription: '%s'
objcpp:
  translator: '::djinni::Color'
  header: '"Basic-objc.hpp"'
java:
  typename: 'int'
  boxed: 'java.lang.Integer'
  reference: true
  generic: false
  hash: '%s.hashCode()'
jni:
  translator: '::djinni::Color'
  header: '"Basic-jni.hpp"'
  typename: jint
  typeSignature: 'I'

In PSPDFKit we do this for many basic types such as Rect, Size, Point, Matrix and Color.

(As an aside, handling errors can understandably get somewhat tricky across platforms and languages. We discuss our approach to passing identifiable errors when using Djinni in our “Failable Methods with Djinni” blog post.)

Platform Services

There are some features where the platform is much better than the C++ layer. It starts with simple things like receiving a low memory notification, showing an alert or even creating a thread or doing network requests. Often there are ways to do this in C++ as well, but they will involve more code, use less-tested paths or are just not possible (the alert). For that, we created an interface called NativeServices. This interface offers various other interfaces for specific tasks. We even went so far and bridged UnicodeServices to have regular expressions without having to ship libicu on Android.

# A place to initialize and retrieve native services.
NativeServices = interface +c {
	# initialize the NativeServices. If no UnicodeService is specified, we will try to pick an appropriate one. If no thread_service is specified, we will rely on a generic thread service that uses `std::thread`.
	static init(application_service: ApplicationService, unicode_service: optional<UnicodeService>, thread_service: optional<PlatformThreads>);

    # Retrieves native application services
    static get_application_services(): ApplicationService;

    # Retrieves the unicode service.
    static get_unicode_service(): UnicodeService;

    # Retrieves the thread service.
    static get_thread_service(): PlatformThreads;

    const low_memory_notification: string = "LowMemoryNotification";

    # Invoked from the native platform to inform about low memory warnings.
    static memory_notification(level: i32);
}

# This is a collection of methods that are very particular to a platform that can’t be easily
# implemented by the C++ core.
ApplicationService = interface +j +o {
    # Shows an alert
    show_alert(title: string, message: string, options: AlertOptions);

    # Checks if we're running in a simulator
    is_simulator(): bool;

    # Returns a temporary directory.
    temporary_directory(): string;
}

Right now most of our API is synchronous — designing API with callbacks is quite hard with djinni and something we’re still tinkering with.

PSPDFKit for the Web

Now we mentioned that we re-use code even for PSPDFKit for the Web, which runs in the browser. Indeed early experiments with emscripten worked well — we simply took our existing C++ core and compiled it to JavaScript. However, moving all processing to the client is not always a good idea. Our Web SDK will also run on smartphones and it should be fast — so instead we build a small wrapper around our core and created a command line interface which can render pages as images and emit JSON for model objects. Our C++ architecture even allows this to compile on Windows — so we’re extremely flexible in terms of backend platform.

Mistakes We Made

Pretty early on we tried to add libdispatch into our C++ core, with the rationale that we can use the native GCD on iOS/Mac and ship our own copy of libdispatch on Android. libdispatch is open source and licensed under Apache 2.0 and there’s an extensive test suite, so this was pretty easy. It requires a compiler that understands the blocks syntax, so it forced us to use Clang on Android. It generally worked well, but caused major headaches with the Android debugging and instrumenting tools. It also made things tricky to compile for Windows, since libdispatch really was written for Unix and as such limits your portability. For a while we switched to the function-based methods but ultimately replaced this with boost::thread. The threads it creates are also problematic when you call up into Java-land with them. While there are ways to connect native threads to the garbage collector, Java really gets weird about this and much prefers threads that have been created in Java. The team at Dropbox recognized this as well and is calling up to Java-land for thread creation, much like we have our PlatformServices.

JNI calls are fairly expensive. Of course there’s data marshaling and that leads to various memory copying and initialization times, but that’s less of a problem on iOS. Android however pays a fairly high tax for JNI calls, and after every step the system requires one to check for pending exceptions. Djinni does a fairly good job of caching the expensive Java reflection part (since classes are looked up via strings), however there’s still some tax to pay. Designing APIs to move large chunks of data over in one call makes things more efficient, but might lead to additional shims or not great APIs in general. We ended up combining Djinni and Flatbuffers, depending on the data set to be both flexible and fast.

What’s Next

Over the last two years we have built a strong, well-tested and powerful C++ project that can render PDFs with annotations and extract data. This project compiles on many platforms, including iOS, OS X, Android and less obvious ones like Windows or even the web. This foundation allowed to to ship our latest feature, the Document Editor to add, remove or rearrange pages on both iOS and Android at the same time.