---
title: "use \"header.h\": C++ interop as a language feature, not a binding layer"
date: "2026-06-20"
description: "Most new UI languages make C/C++ interop second-class — bindings, IDL, schemas. Vel makes a qualified C++ call a normal expression it emits verbatim."
slug: "cpp-interop-as-a-language-feature"
tldr: "Vel's compiler treats C++ as a first-class part of the language: `use \"header.h\"` becomes a verbatim include, the lexer learned `::` so `myapp::db::fetchAll()` parses as a qualified name, and codegen emits the call straight through. The framework knows nothing about your backend — and that power comes with no safety boundary."
tags: ["cpp", "dsl", "compilers", "ffi"]
cover: "/images/blog/cpp-interop-as-a-language-feature.svg"
coverAlt: "A .vel expression calling myapp::db::fetchAll() compiling straight through velc into the equivalent C++ call site"
author: "Sai Chandan Kadarla"
devto_id: 3983607
---

Every "new UI language" I've tried treats talking to existing code as an afterthought. There's a foreign-function interface bolted on the side: you write an IDL, or a schema, or a `extern` block, or you marshal everything through strings and JSON at the boundary. Interop is a *layer* — a place where your nice new language stops and the ugly real world begins.

I built Vel the other way around. The single constraint I set on day one was: **you must be able to drop any existing C++ codebase into a `.vel` file with one line, and call it like it was always there.** Not bindings. Not a boundary. A language feature.

Here's what that actually takes in the compiler.

## The lexer learned `::`

A normal DSL lexer doesn't care about `::`. Vel's does, because the whole interop story hinges on qualified C++ names being first-class tokens, not strings I parse later. So `::` is a real token type (`velc/Lexer.cpp`):

```cpp
tokens_.push_back({TokenType::ColonColon, "::", startLoc});
```

And the parser, when it's reading a name, greedily consumes `::` segments to build a qualified identifier (`velc/Parser.cpp`):

```cpp
while (check(TokenType::ColonColon)) {
    auto next = advance();
    name += "::" + next.text;
}
```

That tiny loop is what makes `std::vector`, `myapp::db::fetchAll`, and `myorg::ui::Theme` parse as single names rather than syntax errors. The grammar didn't need a special "FFI call" construct — qualified names just *are* names, everywhere a name is legal: in expressions, in type annotations, in event handlers.

## `use` is an include, not an import system

There are exactly two `use` forms, and the lexer tags both with one keyword (`use` or `Use`):

```vel
use "backend/users.h"     // raw C++ — emitted verbatim as #include
use "widgets/stat.vel"     // cross-file vel import — pulls in components
```

The C++ form does the least surprising thing possible: it becomes a `#include "backend/users.h"` at the top of the generated translation unit. No parsing of the header, no wrapping, no shadow types. velc doesn't try to *understand* your C++ — it just makes sure it's in scope, and then trusts that the names you use resolve when the C++ compiler runs.

That trust is the design. velc is not a C++ frontend and never tries to be. It type-checks the *Vel* parts — widget props, signal types, the registry — and for anything in a `use`d header, it emits the call and lets the downstream C++ compiler be the type checker. If you typo `myapp::fetchUzers()`, you don't get a velc error; you get a normal C++ "no member named" error pointing at the right place. The DSL borrows the host language's type system instead of duplicating a worse copy of it.

## State can be any C++ type, including async

Because qualified names work in type annotations, your reactive state isn't limited to primitives. This is a real line of Vel:

```vel
#UserList
  @users: std::vector<myapp::User> = await myapp::fetchUsers()
  @filter = ""
```

Three things are happening that only work because interop is in the type system:

- `std::vector<myapp::User>` is a type annotation with a template argument and a qualified name — the parser handles all of it because, again, names are names.
- `myapp::fetchUsers()` is a qualified call in an initializer expression.
- `await` on a call whose result type is a C++ type makes velc emit an `AsyncSignal<std::vector<myapp::User>>`, kicked off with `std::async` and polled each tick — exposing `.loading` / `.ready` / `.value` / `.error` to the DSL.

velc emits, roughly, a `Signal`/`AsyncSignal` member of *your* C++ type, initialized by *your* C++ call. The framework has no idea what `myapp::User` is. It doesn't need to. Your existing code links into the binary as an ordinary translation unit — same compiler, same flags, same linker — and the generated component holds your types directly. There's no serialization at the boundary because there is no boundary.

The same mechanism is why a registry component call compiles to a plain constructor — codegen emits `vel::gen::Stat{...}` the same way it emits `myapp::fetchUsers()`. To velc, framework calls and your calls are the same kind of thing: qualified names it resolves to C++ and hands off.

## What it costs — and it's a real cost

This is the section that separates the design from a sales pitch. Making C++ a language feature means inheriting C++'s consequences with none of the guardrails a binding layer would have given you:

- **There is no safety boundary.** A binding layer is also a *firewall* — it validates, it sandboxes, it catches type mismatches at the edge. Vel has none of that on purpose. If your `use`d function dereferences a null pointer, your UI segfaults, exactly like C++ does. The DSL is not memory-safe across the interop line because there is no line. You're writing C++ with a nicer syntax for the view layer, and you own C++'s footguns.
- **Errors surface one layer down.** A mistake in a qualified call isn't a friendly DSL diagnostic; it's a C++ compiler error in generated code. I keep the generated `.vel.cpp` readable for exactly this reason, but the failure mode is still "read a template error," not "Vel told you nicely."
- **No hot-swap of the native side.** Vel hot-reloads `.vel` files by recompiling and swapping the generated component. But your `use`d C++ is compiled into the binary — change `users.h` and you're doing a real rebuild, not a sub-second reload. The interop is static, which is what makes it zero-overhead and also what makes it not live.
- **It assumes one toolchain.** Because the call sites are literal C++, your backend has to build with the same C++ compiler and ABI as `libvel`. That's fine for a C++ shop; it's not a polyglot story.

I think it's the right trade for what Vel is for: native apps where your data layer is already C++ (or C, or anything with a C++-callable surface), and you want a dense, reactive view language on top without an integration tax. The interop being unsafe is the same reason it's frictionless — the compiler gets out of the way completely.

Every other UI language I've used would make me write a binding for `fetchUsers`. Vel makes me write `await myapp::fetchUsers()`. That difference — interop as a keyword instead of interop as a subproject — is the reason the thing exists at all.
