React Native Land_

// construindo apps mobile melhores

# Uma busca fuzzy em C++ com Nitro Modules

2026-11-12#alto

No post anterior escrevemos uma busca fuzzy em C++ como um Turbo Module. O matcher era um arquivo só, mas em volta dele havia cola em cada plataforma: OnLoad.cpp e CMake no Android, um provider em Objective-C++ no iOS. E o módulo guardava uma única lista para o app inteiro.

Vamos escrever a mesma busca com Nitro Modules. O Nitro consegue implementar uma spec em C++ nas duas plataformas, e o autolinking dele a registra, então a cola desaparece. E um hybrid object é um objeto: cada lista ganha um buscador próprio.

## Crie o módulo

npx nitrogen@latest init react-native-fuzzy

Reduzido ao que importa aqui:

react-native-fuzzy/
  nitro.json
  src/specs/Fuzzy.nitro.ts
  src/index.ts
  cpp/HybridFuzzy.hpp
  cpp/HybridFuzzy.cpp
  nitrogen/generated/

Não há código nosso em ios/ nem em android/: o template compila cpp/ nas duas plataformas, e o nitro.json dá o nome da classe C++ que implementa a spec:

nitro.jsonjson
{
  "cxxNamespace": ["fuzzy"],
  "ios": { "iosModuleName": "NitroFuzzy" },
  "android": {
    "androidNamespace": ["fuzzy"],
    "androidCxxLibName": "NitroFuzzy"
  },
  "autolinking": {
    "Fuzzy": { "cpp": "HybridFuzzy" }
  }
}

## A spec

Os mesmos dois métodos de antes. c++ nas duas plataformas diz ao Nitrogen que não há Swift nem Kotlin para gerar:

src/specs/Fuzzy.nitro.tsts
import type { HybridObject } from 'react-native-nitro-modules';

export interface Fuzzy extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
  setItems(items: string[]): void;
  search(query: string, limit: number): number[];
}
npx nitrogen

O Nitrogen escreve o HybridFuzzySpec, uma classe C++ com um método virtual puro para cada um da spec. Os tipos são C++ comuns: string[] é um std::vector<std::string>, e number é um double. Nenhum jsi::Runtime à vista.

## C++

A classe sobrescreve os métodos da spec. O construtor dá nome ao objeto nos logs do Nitro, e getExternalMemorySize diz ao garbage collector do JavaScript quanta memória nativa ele segura, para que uma lista grande seja coletada a tempo quando ninguém mais a referencia:

cpp/HybridFuzzy.hppcpp
#pragma once

#include "HybridFuzzySpec.hpp"

#include <string>
#include <vector>

namespace margelo::nitro::fuzzy {

class HybridFuzzy : public HybridFuzzySpec {
 public:
  HybridFuzzy() : HybridObject(TAG) {}

  void setItems(const std::vector<std::string>& items) override;
  std::vector<double> search(const std::string& query, double limit) override;

  size_t getExternalMemorySize() noexcept override;

 private:
  std::vector<std::string> items_;
  size_t bytes_ = 0;
};

} // namespace margelo::nitro::fuzzy

O matcher é o do Turbo Module, palavra por palavra. Só as bordas mudam: os argumentos chegam como referências const, e os índices voltam como double:

cpp/HybridFuzzy.cppcpp
#include "HybridFuzzy.hpp"

#include <algorithm>
#include <utility>

namespace margelo::nitro::fuzzy {

namespace {

// ASCII lower case; other bytes pass through, so UTF-8 still matches itself.
char lower(char c) {
  return c >= 'A' && c <= 'Z' ? static_cast<char>(c + ('a' - 'A')) : c;
}

bool startsWord(const std::string& text, size_t i) {
  if (i == 0) return true;
  char before = text[i - 1];
  return before == ' ' || before == '-' || before == '_' || before == '/';
}

// The query's letters in the item, in order: a score, or -1 when one is missing.
int score(const std::string& query, const std::string& item) {
  int total = 0;
  int streak = 0;
  size_t found = 0;
  for (size_t i = 0; i < item.size() && found < query.size(); i++) {
    if (lower(item[i]) != query[found]) {
      streak = 0;
      continue;
    }
    streak++;
    total += 1 + 2 * streak + (startsWord(item, i) ? 8 : 0);
    found++;
  }
  if (found < query.size()) return -1;
  return total - static_cast<int>(item.size() / 8);
}

} // namespace

void HybridFuzzy::setItems(const std::vector<std::string>& items) {
  items_ = items;
  bytes_ = 0;
  for (const auto& item : items_) bytes_ += sizeof(std::string) + item.capacity();
}

std::vector<double> HybridFuzzy::search(const std::string& query, double limit) {
  std::string q;
  q.reserve(query.size());
  for (char c : query) {
    if (c != ' ') q.push_back(lower(c));
  }

  // (score, index) for every item that has all the letters.
  std::vector<std::pair<int, int>> hits;
  hits.reserve(items_.size());
  for (int i = 0; i < static_cast<int>(items_.size()); i++) {
    int s = score(q, items_[i]);
    if (s >= 0) hits.emplace_back(s, i);
  }

  // Only the best `limit` are sorted: a short screen of a long list costs little.
  auto count = std::min(hits.size(), static_cast<size_t>(std::max(limit, 0.0)));
  std::partial_sort(hits.begin(), hits.begin() + count, hits.end(), [](const auto& a, const auto& b) {
    return a.first != b.first ? a.first > b.first : a.second < b.second;
  });

  std::vector<double> best;
  best.reserve(count);
  for (size_t k = 0; k < count; k++) best.push_back(static_cast<double>(hits[k].second));
  return best;
}

size_t HybridFuzzy::getExternalMemorySize() noexcept {
  return bytes_;
}

} // namespace margelo::nitro::fuzzy

As chamadas continuam rodando na thread do JavaScript, de forma síncrona, então items_ não precisa de lock. Para uma lista tão longa que uma busca leve mais de um quadro, a spec poderia devolver uma Promise<number[]>, e o C++ rodá-la em outra thread.

## TypeScript

A entrada do módulo entrega um buscador novo a cada chamada:

src/index.tsts
import { NitroModules } from 'react-native-nitro-modules';
import type { Fuzzy } from './specs/Fuzzy.nitro';

export type { Fuzzy };

export function createFuzzy(): Fuzzy {
  return NitroModules.createHybridObject<Fuzzy>('Fuzzy');
}

Então o hook guarda um por componente. Duas telas buscando em duas listas não compartilham mais nada, e um buscador vai embora junto com o componente:

useFuzzy.tsts
import { useMemo, useState } from 'react';
import { createFuzzy } from 'react-native-fuzzy';

type Item = { id: string; title: string };

export function useFuzzy(items: Item[], query: string, limit = 50): Item[] {
  const [fuzzy] = useState(createFuzzy);

  // setItems só substitui a lista, então rodá-lo de novo com os mesmos itens não faz mal.
  const loaded = useMemo(() => {
    fuzzy.setItems(items.map((item) => item.title));
    return items;
  }, [fuzzy, items]);

  return useMemo(
    () => (query.trim() ? fuzzy.search(query, limit).map((i) => loaded[i]) : loaded),
    [fuzzy, loaded, query, limit],
  );
}

O SearchScreen não muda: useFuzzy(items, state.query) é a mesma chamada que com o Turbo Module.

## Rode

npm install react-native-nitro-modules ./react-native-fuzzy
cd ios && bundle exec pod install && cd ..
npx react-native run-ios
npx react-native run-android

## Turbo ou Nitro, para C++

  • Turbo Modules: nada além do React Native. Em troca, cada plataforma pede a sua cola, o módulo é um por app, e os métodos recebem o jsi::Runtime.
  • Nitro Modules: o mesmo C++ sem cola, tipos C++ comuns, um objeto por lista e um jeito de dizer ao garbage collector o que ele segura. Em troca, uma dependência e um gerador que você mesmo roda.

O matcher não mudou nada entre os dois. É para isso que se escreve em C++: a lógica fica, e só as bordas se mexem. A documentação do Nitro cobre mais tipos C++, incluindo callbacks e Promise.